github.com/rigado/snapd@v2.42.5-go-mod+incompatible/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  // CheckChangeConflictMany ensures that for the given instanceNames no other
    94  // changes that alters the snaps (like remove, install, refresh) are in
    95  // progress. If a conflict is detected an error is returned.
    96  //
    97  // It's like CheckChangeConflict, but for multiple snaps, and does not
    98  // check snapst.
    99  func CheckChangeConflictMany(st *state.State, instanceNames []string, ignoreChangeID string) error {
   100  	snapMap := make(map[string]bool, len(instanceNames))
   101  	for _, k := range instanceNames {
   102  		snapMap[k] = true
   103  	}
   104  
   105  	for _, chg := range st.Changes() {
   106  		if chg.Status().Ready() {
   107  			continue
   108  		}
   109  		if chg.Kind() == "transition-ubuntu-core" {
   110  			return &ChangeConflictError{Message: "ubuntu-core to core transition in progress, no other changes allowed until this is done", ChangeKind: "transition-ubuntu-core"}
   111  		}
   112  		if chg.Kind() == "transition-to-snapd-snap" {
   113  			return &ChangeConflictError{Message: "transition to snapd snap in progress, no other changes allowed until this is done", ChangeKind: "transition-to-snapd-snap"}
   114  		}
   115  		if chg.Kind() == "remodel" {
   116  			if ignoreChangeID != "" && chg.ID() == ignoreChangeID {
   117  				continue
   118  			}
   119  			return &ChangeConflictError{Message: "remodeling in progress, no other changes allowed until this is done", ChangeKind: "remodel"}
   120  		}
   121  	}
   122  
   123  	for _, task := range st.Tasks() {
   124  		chg := task.Change()
   125  		if chg == nil || chg.Status().Ready() {
   126  			continue
   127  		}
   128  		if ignoreChangeID != "" && chg.ID() == ignoreChangeID {
   129  			continue
   130  		}
   131  		if chg.Kind() == "become-operational" {
   132  			// become-operational will be retried until success
   133  			// and on its own just runs a hook on gadget:
   134  			// do not make it interfere with user requests
   135  			// TODO: consider a use vs change modeling of
   136  			// conflicts
   137  			continue
   138  		}
   139  
   140  		snaps, err := affectedSnaps(task)
   141  		if err != nil {
   142  			return err
   143  		}
   144  
   145  		for _, snap := range snaps {
   146  			if snapMap[snap] {
   147  				return &ChangeConflictError{Snap: snap, ChangeKind: chg.Kind()}
   148  			}
   149  		}
   150  	}
   151  
   152  	return nil
   153  }
   154  
   155  // CheckChangeConflict ensures that for the given instanceName no other
   156  // changes that alters the snap (like remove, install, refresh) are in
   157  // progress. It also ensures that snapst (if not nil) did not get
   158  // modified. If a conflict is detected an error is returned.
   159  func CheckChangeConflict(st *state.State, instanceName string, snapst *SnapState) error {
   160  	return checkChangeConflictIgnoringOneChange(st, instanceName, snapst, "")
   161  }
   162  
   163  func checkChangeConflictIgnoringOneChange(st *state.State, instanceName string, snapst *SnapState, ignoreChangeID string) error {
   164  	if err := CheckChangeConflictMany(st, []string{instanceName}, ignoreChangeID); err != nil {
   165  		return err
   166  	}
   167  
   168  	if snapst != nil {
   169  		// caller wants us to also make sure the SnapState in state
   170  		// matches the one they provided. Necessary because we need to
   171  		// unlock while talking to the store, during which a change can
   172  		// sneak in (if it's before the taskset is created) (e.g. for
   173  		// install, while getting the snap info; for refresh, when
   174  		// getting what needs refreshing).
   175  		var cursnapst SnapState
   176  		if err := Get(st, instanceName, &cursnapst); err != nil && err != state.ErrNoState {
   177  			return err
   178  		}
   179  
   180  		// TODO: implement the rather-boring-but-more-performant SnapState.Equals
   181  		if !reflect.DeepEqual(snapst, &cursnapst) {
   182  			return &ChangeConflictError{Snap: instanceName}
   183  		}
   184  	}
   185  
   186  	return nil
   187  }