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

     1  package install
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"time"
     8  
     9  	stacksscheme "github.com/docker/compose-on-kubernetes/api/client/clientset/scheme"
    10  	stacksclient "github.com/docker/compose-on-kubernetes/api/client/clientset/typed/compose/v1beta1"
    11  	stacks "github.com/docker/compose-on-kubernetes/api/compose/v1beta1"
    12  	"github.com/docker/compose-on-kubernetes/api/constants"
    13  	"github.com/docker/compose-on-kubernetes/internal/conversions"
    14  	"github.com/docker/compose-on-kubernetes/internal/internalversion"
    15  	"github.com/docker/compose-on-kubernetes/internal/parsing"
    16  	"github.com/docker/compose-on-kubernetes/internal/registry"
    17  	"github.com/pkg/errors"
    18  	log "github.com/sirupsen/logrus"
    19  	apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
    20  	apiextensionsclient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1"
    21  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    22  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    23  	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    24  	"k8s.io/apimachinery/pkg/util/validation/field"
    25  	"k8s.io/client-go/kubernetes"
    26  	appsv1 "k8s.io/client-go/kubernetes/typed/apps/v1"
    27  	corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
    28  	"k8s.io/client-go/rest"
    29  )
    30  
    31  const (
    32  	// BackupPreviousErase erases previous backup
    33  	BackupPreviousErase = iota
    34  	// BackupPreviousMerge adds/merges new data to previous backup
    35  	BackupPreviousMerge
    36  	// BackupPreviousFail fails if a previous backup exists
    37  	BackupPreviousFail
    38  
    39  	backupAPIGroup    = "composebackup.docker.com"
    40  	userAnnotationKey = "com.docker.compose.user"
    41  )
    42  
    43  func createBackupCrd(crds apiextensionsclient.CustomResourceDefinitionInterface) error {
    44  	log.Info("Creating backup CRD")
    45  	_, err := crds.Create(&apiextensions.CustomResourceDefinition{
    46  		ObjectMeta: v1.ObjectMeta{
    47  			Name: "stackbackups." + backupAPIGroup,
    48  		},
    49  		Spec: apiextensions.CustomResourceDefinitionSpec{
    50  			Group:   backupAPIGroup,
    51  			Version: "v1beta1",
    52  			Names: apiextensions.CustomResourceDefinitionNames{
    53  				Plural:   "stackbackups",
    54  				Singular: "stackbackup",
    55  				Kind:     "StackBackup",
    56  				ListKind: "StackBackupList",
    57  			},
    58  			Scope: apiextensions.NamespaceScoped,
    59  		},
    60  	})
    61  	return err
    62  }
    63  
    64  func copyStacksToBackupCrd(source stacks.StackList, kubeClient kubernetes.Interface) error {
    65  	for _, stack := range source.Items {
    66  		stack.APIVersion = fmt.Sprintf("%s/v1beta1", backupAPIGroup)
    67  		stack.ResourceVersion = ""
    68  		stack.Kind = "StackBackup"
    69  		jstack, err := json.Marshal(stack)
    70  		if err != nil {
    71  			return errors.Wrap(err, "failed to marshal stack to JSON")
    72  		}
    73  		res := kubeClient.CoreV1().RESTClient().Verb("POST").
    74  			RequestURI(fmt.Sprintf("/apis/%s/v1beta1/namespaces/%s/stackbackups", backupAPIGroup, stack.Namespace)).
    75  			Body(jstack).Do()
    76  		if res.Error() != nil {
    77  			if apierrors.IsAlreadyExists(res.Error()) {
    78  				// stack already exists, try updating it
    79  				updateRes := kubeClient.CoreV1().RESTClient().Verb("PUT").
    80  					RequestURI(fmt.Sprintf("/apis/%s/v1beta1/namespaces/%s/stackbackups", backupAPIGroup, stack.Namespace)).
    81  					Body(jstack).Do()
    82  				if updateRes.Error() == nil {
    83  					continue
    84  				} else {
    85  					return errors.Wrap(updateRes.Error(), fmt.Sprintf("failed to write then update stack %s/%s", stack.Namespace, stack.Name))
    86  				}
    87  			}
    88  			return errors.Wrap(res.Error(), fmt.Sprintf("failed to write stack %s/%s", stack.Namespace, stack.Name))
    89  		}
    90  	}
    91  	return nil
    92  }
    93  
    94  // Backup saves all stacks to a new temporary CRD
    95  func Backup(config *rest.Config, mode int) error {
    96  	log.Info("Starting backup process")
    97  	extClient, err := apiextensionsclient.NewForConfig(config)
    98  	if err != nil {
    99  		return err
   100  	}
   101  	crds := extClient.CustomResourceDefinitions()
   102  	_, err = crds.Get("stackbackups."+backupAPIGroup, v1.GetOptions{})
   103  	needsCreate := err != nil
   104  	if err == nil {
   105  		switch mode {
   106  		case BackupPreviousFail:
   107  			return errors.New("a previous backup already exists")
   108  		case BackupPreviousErase:
   109  			log.Info("Erasing previous backup")
   110  			err = crds.Delete("stackbackups."+backupAPIGroup, &v1.DeleteOptions{})
   111  			if err != nil {
   112  				return err
   113  			}
   114  			for i := 0; i < 60; i++ {
   115  				_, err = crds.Get("stackbackups."+backupAPIGroup, v1.GetOptions{})
   116  				if err != nil {
   117  					break
   118  				}
   119  				time.Sleep(1 * time.Second)
   120  			}
   121  			needsCreate = true
   122  		case BackupPreviousMerge:
   123  			log.Info("Merging with previous backup")
   124  		}
   125  	}
   126  	if needsCreate {
   127  		if err := createBackupCrd(crds); err != nil {
   128  			return err
   129  		}
   130  	}
   131  	log.Info("Copying stacks to backup CRD")
   132  	// The stacks client will work both with apiserver and crd backed resource
   133  	kubeClient, err := kubernetes.NewForConfig(config)
   134  	if err != nil {
   135  		return err
   136  	}
   137  	var source stacks.StackList
   138  	listOpts := metav1.ListOptions{}
   139  	for {
   140  		err = kubeClient.CoreV1().RESTClient().Verb("GET").
   141  			RequestURI("/apis/compose.docker.com/v1beta1/stacks").
   142  			VersionedParams(&listOpts, stacksscheme.ParameterCodec).
   143  			Do().
   144  			Into(&source)
   145  		if err != nil {
   146  			return err
   147  		}
   148  		if err = copyStacksToBackupCrd(source, kubeClient); err != nil {
   149  			return err
   150  		}
   151  		if source.Continue == "" {
   152  			break
   153  		}
   154  		listOpts.Continue = source.Continue
   155  	}
   156  	return nil
   157  }
   158  
   159  // Restore copies stacks from backup to v1beta1 stacks.compose.docker.com
   160  func Restore(baseConfig *rest.Config, impersonate bool) (map[string]error, error) {
   161  	log.Info("Restoring stacks from backup")
   162  	kubeClient, err := kubernetes.NewForConfig(baseConfig)
   163  	if err != nil {
   164  		return nil, err
   165  	}
   166  	var (
   167  		source   stacks.StackList
   168  		listOpts metav1.ListOptions
   169  	)
   170  	stackErrs := make(map[string]error)
   171  	config := *baseConfig
   172  	client, err := stacksclient.NewForConfig(&config)
   173  	if err != nil {
   174  		return nil, err
   175  	}
   176  
   177  	for {
   178  		err = kubeClient.CoreV1().RESTClient().Verb("GET").
   179  			RequestURI(fmt.Sprintf("/apis/%s/v1beta1/stackbackups", backupAPIGroup)).
   180  			VersionedParams(&listOpts, stacksscheme.ParameterCodec).
   181  			Do().
   182  			Into(&source)
   183  		if err != nil {
   184  			return nil, err
   185  		}
   186  
   187  		for _, stack := range source.Items {
   188  			stack.APIVersion = "compose.docker.com/v1beta1"
   189  			stack.Kind = "Stack"
   190  			stack.ResourceVersion = ""
   191  			if impersonate {
   192  				username := ""
   193  				if stack.Annotations != nil {
   194  					username = stack.Annotations[userAnnotationKey]
   195  					delete(stack.Annotations, userAnnotationKey)
   196  				}
   197  				if config.Impersonate.UserName != username {
   198  					config.Impersonate.UserName = username
   199  					log.Infof("Impersonating user %q", username)
   200  					if client, err = stacksclient.NewForConfig(&config); err != nil {
   201  						return nil, err
   202  					}
   203  				}
   204  			}
   205  			_, err = client.Stacks(stack.Namespace).WithSkipValidation().Create(&stack)
   206  			if err != nil {
   207  				stackErrs[fmt.Sprintf("%s/%s", stack.Namespace, stack.Name)] = err
   208  				if !apierrors.IsAlreadyExists(err) {
   209  					return stackErrs, errors.Wrap(err, "unable to restore stacks")
   210  				}
   211  			}
   212  		}
   213  		if source.Continue == "" {
   214  			break
   215  		}
   216  		listOpts.Continue = source.Continue
   217  	}
   218  	return stackErrs, nil
   219  }
   220  
   221  func dryRunStacks(source stacks.StackList, res map[string]error, coreClient corev1.ServicesGetter, appsClient appsv1.AppsV1Interface) error {
   222  	for _, stack := range source.Items {
   223  		fullname := fmt.Sprintf("%s/%s", stack.Namespace, stack.Name)
   224  		composeConfig, err := parsing.LoadStackData([]byte(stack.Spec.ComposeFile), nil)
   225  		if err != nil {
   226  			res[fullname] = err
   227  			continue
   228  		}
   229  		spec := conversions.FromComposeConfig(composeConfig)
   230  		internalStack := &internalversion.Stack{
   231  			ObjectMeta: v1.ObjectMeta{
   232  				Name:      stack.Name,
   233  				Namespace: stack.Namespace,
   234  			},
   235  			Spec: internalversion.StackSpec{
   236  				Stack: spec,
   237  			},
   238  		}
   239  		errs := field.ErrorList{}
   240  		errs = append(errs, registry.ValidateObjectNames(internalStack)...)
   241  		errs = append(errs, registry.ValidateDryRun(internalStack)...)
   242  		errs = append(errs, registry.ValidateCollisions(coreClient, appsClient, internalStack)...)
   243  		aggregate := errs.ToAggregate()
   244  		if aggregate != nil {
   245  			res[fullname] = aggregate
   246  		}
   247  	}
   248  	return nil
   249  }
   250  
   251  // DryRun checks existing stacks for conversion errors or conflicts
   252  func DryRun(config *rest.Config) (map[string]error, error) {
   253  	res := make(map[string]error)
   254  	log.Info("Performing dry-run")
   255  	kubeClient, err := kubernetes.NewForConfig(config)
   256  	if err != nil {
   257  		return nil, err
   258  	}
   259  	var source stacks.StackList
   260  	listOpts := metav1.ListOptions{}
   261  	for {
   262  		err = kubeClient.CoreV1().RESTClient().Verb("GET").
   263  			RequestURI("/apis/compose.docker.com/v1beta1/stacks").
   264  			VersionedParams(&listOpts, stacksscheme.ParameterCodec).
   265  			Do().
   266  			Into(&source)
   267  		if err != nil {
   268  			return nil, err
   269  		}
   270  		if err = dryRunStacks(source, res, kubeClient.CoreV1(), kubeClient.AppsV1()); err != nil {
   271  			return nil, err
   272  		}
   273  		if source.Continue == "" {
   274  			break
   275  		}
   276  		listOpts.Continue = source.Continue
   277  	}
   278  	return res, nil
   279  }
   280  
   281  // DeleteBackup deletes the backup CRD
   282  func DeleteBackup(config *rest.Config) error {
   283  	extClient, err := apiextensionsclient.NewForConfig(config)
   284  	if err != nil {
   285  		return err
   286  	}
   287  	crds := extClient.CustomResourceDefinitions()
   288  	_, err = crds.Get("stackbackups."+backupAPIGroup, v1.GetOptions{})
   289  	if err == nil {
   290  		return crds.Delete("stackbackups."+backupAPIGroup, &v1.DeleteOptions{})
   291  	}
   292  	if err != nil && !apierrors.IsNotFound(err) {
   293  		return err
   294  	}
   295  	for {
   296  		_, err = crds.Get("stackbackups."+backupAPIGroup, metav1.GetOptions{})
   297  		if err != nil {
   298  			if apierrors.IsNotFound(err) {
   299  				return nil
   300  			}
   301  			return err
   302  		}
   303  		time.Sleep(time.Second)
   304  	}
   305  }
   306  
   307  // HasBackupCRD indicates if the backup crd is there
   308  func HasBackupCRD(config *rest.Config) (bool, error) {
   309  	extClient, err := apiextensionsclient.NewForConfig(config)
   310  	if err != nil {
   311  		return false, err
   312  	}
   313  	crds := extClient.CustomResourceDefinitions()
   314  	_, err = crds.Get("stackbackups."+backupAPIGroup, v1.GetOptions{})
   315  	if err == nil {
   316  		return true, nil
   317  	}
   318  	if apierrors.IsNotFound(err) {
   319  		return false, nil
   320  	}
   321  	return false, err
   322  }
   323  
   324  // CRDCRD installs the CRD component of CRD install
   325  func CRDCRD(config *rest.Config) error {
   326  	extClient, err := apiextensionsclient.NewForConfig(config)
   327  	if err != nil {
   328  		return err
   329  	}
   330  	crds := extClient.CustomResourceDefinitions()
   331  	_, err = crds.Create(&apiextensions.CustomResourceDefinition{
   332  		ObjectMeta: v1.ObjectMeta{
   333  			Name: "stacks.compose.docker.com",
   334  		},
   335  		Spec: apiextensions.CustomResourceDefinitionSpec{
   336  			Group:   "compose.docker.com",
   337  			Version: "v1beta1",
   338  			Names: apiextensions.CustomResourceDefinitionNames{
   339  				Plural:   "stacks",
   340  				Singular: "stack",
   341  				Kind:     "Stack",
   342  				ListKind: "StackList",
   343  			},
   344  			Scope: apiextensions.NamespaceScoped,
   345  		},
   346  	})
   347  	return err
   348  }
   349  
   350  // UninstallComposeCRD uninstalls compose in CRD mode, preserving running stacks
   351  func UninstallComposeCRD(config *rest.Config, namespace string) error {
   352  	if err := Uninstall(config, namespace, true); err != nil {
   353  		return err
   354  	}
   355  	WaitForUninstallCompletion(context.Background(), config, namespace, true)
   356  	if err := UninstallCRD(config); err != nil {
   357  		return err
   358  	}
   359  	WaitForUninstallCompletion(context.Background(), config, namespace, false)
   360  	return nil
   361  }
   362  
   363  // UninstallComposeAPIServer uninstalls compose in API server mode, preserving running stacks
   364  func UninstallComposeAPIServer(config *rest.Config, namespace string) error {
   365  	// First, shoot the controller
   366  	log.Info("Removing controller")
   367  	apps, err := appsv1.NewForConfig(config)
   368  	if err != nil {
   369  		return err
   370  	}
   371  	err = apps.Deployments(namespace).Delete("compose", &metav1.DeleteOptions{})
   372  	if err != nil && !apierrors.IsNotFound(err) {
   373  		return err
   374  	}
   375  	log.Info("Unlinking stacks")
   376  	if err := UninstallCRD(config); err != nil {
   377  		return err
   378  	}
   379  	log.Info("Uninstalling all components")
   380  	if err := Uninstall(config, namespace, false); err != nil {
   381  		return err
   382  	}
   383  	log.Info("Waiting for uninstallation to complete")
   384  	WaitForUninstallCompletion(context.Background(), config, namespace, false)
   385  	return nil
   386  }
   387  
   388  // Update perform a full update operation, restoring the stacks
   389  func Update(config *rest.Config, namespace, tag string, abortOnError bool) (map[string]error, error) {
   390  	if abortOnError {
   391  		errs, err := DryRun(config)
   392  		if err != nil {
   393  			return errs, err
   394  		}
   395  		if len(errs) != 0 {
   396  			return errs, errors.New("dry-run returned errors")
   397  		}
   398  	}
   399  	err := Backup(config, BackupPreviousErase)
   400  	if err != nil {
   401  		return nil, err
   402  	}
   403  	err = UninstallComposeCRD(config, namespace)
   404  	if err != nil {
   405  		return nil, err
   406  	}
   407  	installOptAPIAggregation := WithUnsafe(UnsafeOptions{
   408  		OptionsCommon: OptionsCommon{
   409  			Namespace:              namespace,
   410  			Tag:                    tag,
   411  			ReconciliationInterval: constants.DefaultFullSyncInterval,
   412  		},
   413  	})
   414  	err = Do(context.Background(), config, installOptAPIAggregation, WithoutController())
   415  	if err != nil {
   416  		return nil, err
   417  	}
   418  	ready := false
   419  	for i := 0; i < 30; i++ {
   420  		running, err := IsRunning(config)
   421  		if err != nil {
   422  			return nil, err
   423  		}
   424  		if running {
   425  			ready = true
   426  			break
   427  		}
   428  		time.Sleep(time.Second)
   429  	}
   430  	if !ready {
   431  		return nil, errors.New("compose did not start properly")
   432  	}
   433  	errs, err := Restore(config, true)
   434  	err2 := Do(context.Background(), config, installOptAPIAggregation, WithControllerOnly())
   435  	if err2 != nil {
   436  		return errs, err2
   437  	}
   438  	return errs, err
   439  }