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 }