github.com/kubiko/snapd@v0.0.0-20201013125620-d4f3094d9ddf/asserts/snapasserts/validation_sets.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2020 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 snapasserts
    21  
    22  import (
    23  	"bytes"
    24  	"fmt"
    25  	"sort"
    26  	"strings"
    27  
    28  	"github.com/snapcore/snapd/asserts"
    29  	"github.com/snapcore/snapd/snap"
    30  )
    31  
    32  // ValidationSetsConflictError describes an error where multiple
    33  // validation sets are in conflict about snaps.
    34  type ValidationSetsConflictError struct {
    35  	Sets  map[string]*asserts.ValidationSet
    36  	Snaps map[string]error
    37  }
    38  
    39  func (e *ValidationSetsConflictError) Error() string {
    40  	buf := bytes.NewBufferString("validation sets are in conflict:")
    41  	for _, err := range e.Snaps {
    42  		fmt.Fprintf(buf, "\n- %v", err)
    43  	}
    44  	return buf.String()
    45  }
    46  
    47  // ValidationSets can hold a combination of validation-set assertions
    48  // and can check for conflicts or help applying them.
    49  type ValidationSets struct {
    50  	// sets maps sequence keys to validation-set in the combination
    51  	sets map[string]*asserts.ValidationSet
    52  	// snaps maps snap-ids to snap constraints
    53  	snaps map[string]*snapContraints
    54  }
    55  
    56  const presConflict asserts.Presence = "conflict"
    57  
    58  var unspecifiedRevision = snap.R(0)
    59  var invalidPresRevision = snap.R(-1)
    60  
    61  type snapContraints struct {
    62  	name     string
    63  	presence asserts.Presence
    64  	// revisions maps revisions to pairing of ValidationSetSnap
    65  	// and the originating validation-set key
    66  	// * unspecifiedRevision is used for constraints without a
    67  	//   revision
    68  	// * invalidPresRevision is used for constraints that mark
    69  	//   presence as invalid
    70  	revisions map[snap.Revision][]*revConstraint
    71  }
    72  
    73  type revConstraint struct {
    74  	validationSetKey string
    75  	asserts.ValidationSetSnap
    76  }
    77  
    78  func (c *snapContraints) conflict() *snapConflictsError {
    79  	if c.presence != presConflict {
    80  		return nil
    81  	}
    82  
    83  	const dontCare asserts.Presence = ""
    84  	whichSets := func(rcs []*revConstraint, presence asserts.Presence) []string {
    85  		which := make([]string, 0, len(rcs))
    86  		for _, rc := range rcs {
    87  			if presence != dontCare && rc.Presence != presence {
    88  				continue
    89  			}
    90  			which = append(which, rc.validationSetKey)
    91  		}
    92  		if len(which) == 0 {
    93  			return nil
    94  		}
    95  		sort.Strings(which)
    96  		return which
    97  	}
    98  
    99  	byRev := make(map[snap.Revision][]string, len(c.revisions))
   100  	for r := range c.revisions {
   101  		pres := dontCare
   102  		switch r {
   103  		case invalidPresRevision:
   104  			pres = asserts.PresenceInvalid
   105  		case unspecifiedRevision:
   106  			pres = asserts.PresenceRequired
   107  		}
   108  		which := whichSets(c.revisions[r], pres)
   109  		if len(which) != 0 {
   110  			byRev[r] = which
   111  		}
   112  	}
   113  
   114  	return &snapConflictsError{
   115  		name:      c.name,
   116  		revisions: byRev,
   117  	}
   118  }
   119  
   120  type snapConflictsError struct {
   121  	name string
   122  	// revisions maps revisions to validation-set keys of the sets
   123  	// that are in conflict over the revision.
   124  	// * unspecifiedRevision is used for validation-sets conflicting
   125  	//   on the snap by requiring it but without a revision
   126  	// * invalidPresRevision is used for validation-sets that mark
   127  	//   presence as invalid
   128  	// see snapContraints.revisions as well
   129  	revisions map[snap.Revision][]string
   130  }
   131  
   132  func (e *snapConflictsError) Error() string {
   133  	whichSets := func(which []string) string {
   134  		return fmt.Sprintf("(%s)", strings.Join(which, ","))
   135  	}
   136  
   137  	msg := fmt.Sprintf("cannot constrain snap %q", e.name)
   138  	invalid := false
   139  	if invalidOnes, ok := e.revisions[invalidPresRevision]; ok {
   140  		msg += fmt.Sprintf(" as both invalid %s and required", whichSets(invalidOnes))
   141  		invalid = true
   142  	}
   143  
   144  	var revnos []int
   145  	for r := range e.revisions {
   146  		if r.N >= 1 {
   147  			revnos = append(revnos, r.N)
   148  		}
   149  	}
   150  	if len(revnos) == 1 {
   151  		msg += fmt.Sprintf(" at revision %d %s", revnos[0], whichSets(e.revisions[snap.R(revnos[0])]))
   152  	} else if len(revnos) > 1 {
   153  		sort.Ints(revnos)
   154  		l := make([]string, 0, len(revnos))
   155  		for _, rev := range revnos {
   156  			l = append(l, fmt.Sprintf("%d %s", rev, whichSets(e.revisions[snap.R(rev)])))
   157  		}
   158  		msg += fmt.Sprintf(" at different revisions %s", strings.Join(l, ", "))
   159  	}
   160  
   161  	if unspecifiedOnes, ok := e.revisions[unspecifiedRevision]; ok {
   162  		which := whichSets(unspecifiedOnes)
   163  		if which != "" {
   164  			if len(revnos) != 0 {
   165  				msg += " or"
   166  			}
   167  			if invalid {
   168  				msg += fmt.Sprintf(" at any revision %s", which)
   169  			} else {
   170  				msg += fmt.Sprintf(" required at any revision %s", which)
   171  			}
   172  		}
   173  	}
   174  	return msg
   175  }
   176  
   177  // NewValidationSets returns a new ValidationSets.
   178  func NewValidationSets() *ValidationSets {
   179  	return &ValidationSets{
   180  		sets:  map[string]*asserts.ValidationSet{},
   181  		snaps: map[string]*snapContraints{},
   182  	}
   183  }
   184  
   185  func valSetKey(valset *asserts.ValidationSet) string {
   186  	return fmt.Sprintf("%s/%s", valset.AccountID(), valset.Name())
   187  }
   188  
   189  // Add adds the given asserts.ValidationSet to the combination.
   190  // It errors if a validation-set with the same sequence key has been
   191  // added already.
   192  func (v *ValidationSets) Add(valset *asserts.ValidationSet) error {
   193  	k := valSetKey(valset)
   194  	if _, ok := v.sets[k]; ok {
   195  		return fmt.Errorf("cannot add a second validation-set under %q", k)
   196  	}
   197  	v.sets[k] = valset
   198  	for _, sn := range valset.Snaps() {
   199  		v.addSnap(sn, k)
   200  	}
   201  	return nil
   202  }
   203  
   204  func (v *ValidationSets) addSnap(sn *asserts.ValidationSetSnap, validationSetKey string) {
   205  	rev := snap.R(sn.Revision)
   206  	if sn.Presence == asserts.PresenceInvalid {
   207  		rev = invalidPresRevision
   208  	}
   209  
   210  	rc := &revConstraint{
   211  		validationSetKey:  validationSetKey,
   212  		ValidationSetSnap: *sn,
   213  	}
   214  
   215  	cs := v.snaps[sn.SnapID]
   216  	if cs == nil {
   217  		v.snaps[sn.SnapID] = &snapContraints{
   218  			name:     sn.Name,
   219  			presence: sn.Presence,
   220  			revisions: map[snap.Revision][]*revConstraint{
   221  				rev: {rc},
   222  			},
   223  		}
   224  		return
   225  	}
   226  
   227  	cs.revisions[rev] = append(cs.revisions[rev], rc)
   228  	if cs.presence == presConflict {
   229  		// nothing to check anymore
   230  		return
   231  	}
   232  	// this counts really different revisions or invalid
   233  	ndiff := len(cs.revisions)
   234  	if _, ok := cs.revisions[unspecifiedRevision]; ok {
   235  		ndiff -= 1
   236  	}
   237  	switch {
   238  	case cs.presence == asserts.PresenceOptional:
   239  		cs.presence = sn.Presence
   240  		fallthrough
   241  	case cs.presence == sn.Presence || sn.Presence == asserts.PresenceOptional:
   242  		if ndiff > 1 {
   243  			if cs.presence == asserts.PresenceRequired {
   244  				// different revisions required/invalid
   245  				cs.presence = presConflict
   246  				return
   247  			}
   248  			// multiple optional at different revisions => invalid
   249  			cs.presence = asserts.PresenceInvalid
   250  		}
   251  		return
   252  	}
   253  	// we are left with a combo of required and invalid => conflict
   254  	cs.presence = presConflict
   255  	return
   256  }
   257  
   258  // Conflict returns a non-nil error if the combination is in conflict,
   259  // nil otherwise.
   260  func (v *ValidationSets) Conflict() error {
   261  	sets := make(map[string]*asserts.ValidationSet)
   262  	snaps := make(map[string]error)
   263  
   264  	for snapID, snConstrs := range v.snaps {
   265  		snConflictsErr := snConstrs.conflict()
   266  		if snConflictsErr != nil {
   267  			snaps[snapID] = snConflictsErr
   268  			for _, valsetKeys := range snConflictsErr.revisions {
   269  				for _, valsetKey := range valsetKeys {
   270  					sets[valsetKey] = v.sets[valsetKey]
   271  				}
   272  			}
   273  		}
   274  	}
   275  
   276  	if len(snaps) != 0 {
   277  		return &ValidationSetsConflictError{
   278  			Sets:  sets,
   279  			Snaps: snaps,
   280  		}
   281  	}
   282  	return nil
   283  }