github.com/freetocompute/snapd@v0.0.0-20210618182524-2fb355d72fd9/overlord/snapstate/conflict.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2016-2018 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package snapstate
    21  
    22  import (
    23  	"fmt"
    24  	"reflect"
    25  
    26  	"github.com/snapcore/snapd/overlord/state"
    27  )
    28  
    29  // FinalTasks are task kinds for final tasks in a change which means no further
    30  // change work should be performed afterward, usually these are tasks that
    31  // commit a full system transition.
    32  var FinalTasks = []string{"mark-seeded", "set-model"}
    33  
    34  // ChangeConflictError represents an error because of snap conflicts between changes.
    35  type ChangeConflictError struct {
    36  	Snap       string
    37  	ChangeKind string
    38  	// a Message is optional, otherwise one is composed from the other information
    39  	Message string
    40  }
    41  
    42  func (e *ChangeConflictError) Error() string {
    43  	if e.Message != "" {
    44  		return e.Message
    45  	}
    46  	if e.ChangeKind != "" {
    47  		return fmt.Sprintf("snap %q has %q change in progress", e.Snap, e.ChangeKind)
    48  	}
    49  	return fmt.Sprintf("snap %q has changes in progress", e.Snap)
    50  }
    51  
    52  // An AffectedSnapsFunc returns a list of affected snap names for the given supported task.
    53  type AffectedSnapsFunc func(*state.Task) ([]string, error)
    54  
    55  var (
    56  	affectedSnapsByAttr = make(map[string]AffectedSnapsFunc)
    57  	affectedSnapsByKind = make(map[string]AffectedSnapsFunc)
    58  )
    59  
    60  // AddAffectedSnapsByAttrs registers an AffectedSnapsFunc for returning the affected snaps for tasks sporting the given identifying attribute, to use in conflicts detection.
    61  func AddAffectedSnapsByAttr(attr string, f AffectedSnapsFunc) {
    62  	affectedSnapsByAttr[attr] = f
    63  }
    64  
    65  // AddAffectedSnapsByKind registers an AffectedSnapsFunc for returning the affected snaps for tasks of the given kind, to use in conflicts detection. Whenever possible using AddAffectedSnapsByAttr should be preferred.
    66  func AddAffectedSnapsByKind(kind string, f AffectedSnapsFunc) {
    67  	affectedSnapsByKind[kind] = f
    68  }
    69  
    70  func affectedSnaps(t *state.Task) ([]string, error) {
    71  	// snapstate's own styled tasks
    72  	if t.Has("snap-setup") || t.Has("snap-setup-task") {
    73  		snapsup, err := TaskSnapSetup(t)
    74  		if err != nil {
    75  			return nil, fmt.Errorf("internal error: cannot obtain snap setup from task: %s", t.Summary())
    76  		}
    77  		return []string{snapsup.InstanceName()}, nil
    78  	}
    79  
    80  	if f := affectedSnapsByKind[t.Kind()]; f != nil {
    81  		return f(t)
    82  	}
    83  
    84  	for attrKey, f := range affectedSnapsByAttr {
    85  		if t.Has(attrKey) {
    86  			return f(t)
    87  		}
    88  	}
    89  
    90  	return nil, nil
    91  }
    92  
    93  func checkChangeConflictExclusiveKinds(st *state.State, newExclusiveChangeKind, ignoreChangeID string) error {
    94  	for _, chg := range st.Changes() {
    95  		if chg.Status().Ready() {
    96  			continue
    97  		}
    98  		switch chg.Kind() {
    99  		case "transition-ubuntu-core":
   100  			return &ChangeConflictError{
   101  				Message:    "ubuntu-core to core transition in progress, no other changes allowed until this is done",
   102  				ChangeKind: "transition-ubuntu-core",
   103  			}
   104  		case "transition-to-snapd-snap":
   105  			return &ChangeConflictError{
   106  				Message:    "transition to snapd snap in progress, no other changes allowed until this is done",
   107  				ChangeKind: "transition-to-snapd-snap",
   108  			}
   109  		case "remodel":
   110  			if ignoreChangeID != "" && chg.ID() == ignoreChangeID {
   111  				continue
   112  			}
   113  			return &ChangeConflictError{
   114  				Message:    "remodeling in progress, no other changes allowed until this is done",
   115  				ChangeKind: "remodel",
   116  			}
   117  		case "create-recovery-system":
   118  			if ignoreChangeID != "" && chg.ID() == ignoreChangeID {
   119  				continue
   120  			}
   121  			return &ChangeConflictError{
   122  				Message:    "creating recovery system in progress, no other changes allowed until this is done",
   123  				ChangeKind: "create-recovery-system",
   124  			}
   125  		default:
   126  			if newExclusiveChangeKind != "" {
   127  				// we want to run a new exclusive change, but other
   128  				// changes are in progress already
   129  				msg := fmt.Sprintf("other changes in progress (conflicting change %q), change %q not allowed until they are done", chg.Kind(),
   130  					newExclusiveChangeKind)
   131  				return &ChangeConflictError{
   132  					Message:    msg,
   133  					ChangeKind: chg.Kind(),
   134  				}
   135  			}
   136  		}
   137  	}
   138  	return nil
   139  }
   140  
   141  // CheckChangeConflictRunExclusively checks for conflicts with a new change which
   142  // must be run when no other changes are running.
   143  func CheckChangeConflictRunExclusively(st *state.State, newChangeKind string) error {
   144  	return checkChangeConflictExclusiveKinds(st, newChangeKind, "")
   145  }
   146  
   147  // CheckChangeConflictMany ensures that for the given instanceNames no other
   148  // changes that alters the snaps (like remove, install, refresh) are in
   149  // progress. If a conflict is detected an error is returned.
   150  //
   151  // It's like CheckChangeConflict, but for multiple snaps, and does not
   152  // check snapst.
   153  func CheckChangeConflictMany(st *state.State, instanceNames []string, ignoreChangeID string) error {
   154  	snapMap := make(map[string]bool, len(instanceNames))
   155  	for _, k := range instanceNames {
   156  		snapMap[k] = true
   157  	}
   158  
   159  	// check whether there are other changes that need to run exclusively
   160  	if err := checkChangeConflictExclusiveKinds(st, "", ignoreChangeID); err != nil {
   161  		return err
   162  	}
   163  
   164  	for _, task := range st.Tasks() {
   165  		chg := task.Change()
   166  		if chg == nil || chg.Status().Ready() {
   167  			continue
   168  		}
   169  		if ignoreChangeID != "" && chg.ID() == ignoreChangeID {
   170  			continue
   171  		}
   172  		if chg.Kind() == "become-operational" {
   173  			// become-operational will be retried until success
   174  			// and on its own just runs a hook on gadget:
   175  			// do not make it interfere with user requests
   176  			// TODO: consider a use vs change modeling of
   177  			// conflicts
   178  			continue
   179  		}
   180  
   181  		snaps, err := affectedSnaps(task)
   182  		if err != nil {
   183  			return err
   184  		}
   185  
   186  		for _, snap := range snaps {
   187  			if snapMap[snap] {
   188  				return &ChangeConflictError{Snap: snap, ChangeKind: chg.Kind()}
   189  			}
   190  		}
   191  	}
   192  
   193  	return nil
   194  }
   195  
   196  // CheckChangeConflict ensures that for the given instanceName no other
   197  // changes that alters the snap (like remove, install, refresh) are in
   198  // progress. It also ensures that snapst (if not nil) did not get
   199  // modified. If a conflict is detected an error is returned.
   200  func CheckChangeConflict(st *state.State, instanceName string, snapst *SnapState) error {
   201  	return checkChangeConflictIgnoringOneChange(st, instanceName, snapst, "")
   202  }
   203  
   204  func checkChangeConflictIgnoringOneChange(st *state.State, instanceName string, snapst *SnapState, ignoreChangeID string) error {
   205  	if err := CheckChangeConflictMany(st, []string{instanceName}, ignoreChangeID); err != nil {
   206  		return err
   207  	}
   208  
   209  	if snapst != nil {
   210  		// caller wants us to also make sure the SnapState in state
   211  		// matches the one they provided. Necessary because we need to
   212  		// unlock while talking to the store, during which a change can
   213  		// sneak in (if it's before the taskset is created) (e.g. for
   214  		// install, while getting the snap info; for refresh, when
   215  		// getting what needs refreshing).
   216  		var cursnapst SnapState
   217  		if err := Get(st, instanceName, &cursnapst); err != nil && err != state.ErrNoState {
   218  			return err
   219  		}
   220  
   221  		// TODO: implement the rather-boring-but-more-performant SnapState.Equals
   222  		if !reflect.DeepEqual(snapst, &cursnapst) {
   223  			return &ChangeConflictError{Snap: instanceName}
   224  		}
   225  	}
   226  
   227  	return nil
   228  }