gitee.com/mysnapcore/mysnapd@v0.1.0/store/tooling/tooling.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2014-2022 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 tooling
    21  
    22  import (
    23  	"context"
    24  	"crypto"
    25  	"errors"
    26  	"fmt"
    27  	"io"
    28  	"net/url"
    29  	"os"
    30  	"os/signal"
    31  	"path/filepath"
    32  	"strings"
    33  	"syscall"
    34  
    35  	"gitee.com/mysnapcore/mysnapd/asserts"
    36  	"gitee.com/mysnapcore/mysnapd/logger"
    37  	"gitee.com/mysnapcore/mysnapd/osutil"
    38  	"gitee.com/mysnapcore/mysnapd/overlord/auth"
    39  	"gitee.com/mysnapcore/mysnapd/progress"
    40  	"gitee.com/mysnapcore/mysnapd/snap"
    41  	"gitee.com/mysnapcore/mysnapd/snap/naming"
    42  	"gitee.com/mysnapcore/mysnapd/store"
    43  	"gitee.com/mysnapcore/mysnapd/strutil"
    44  )
    45  
    46  // ToolingStore wraps access to the store for tools.
    47  type ToolingStore struct {
    48  	// Stdout is for output, mainly progress bars
    49  	// left unset stdout is used
    50  	Stdout io.Writer
    51  
    52  	sto StoreImpl
    53  	cfg *store.Config
    54  }
    55  
    56  // A StoreImpl can find metadata on snaps, download snaps and fetch assertions.
    57  // This interface is a subset of store.Store methods.
    58  type StoreImpl interface {
    59  	// SnapAction queries the store for snap information for the given install/refresh actions. Orthogonally it can be used to fetch or update assertions.
    60  	SnapAction(context.Context, []*store.CurrentSnap, []*store.SnapAction, store.AssertionQuery, *auth.UserState, *store.RefreshOptions) ([]store.SnapActionResult, []store.AssertionResult, error)
    61  
    62  	// Download downloads the snap addressed by download info
    63  	Download(ctx context.Context, name, targetFn string, downloadInfo *snap.DownloadInfo, pbar progress.Meter, user *auth.UserState, dlOpts *store.DownloadOptions) error
    64  
    65  	// Assertion retrieves the assertion for the given type and primary key.
    66  	Assertion(assertType *asserts.AssertionType, primaryKey []string, user *auth.UserState) (asserts.Assertion, error)
    67  }
    68  
    69  func newToolingStore(arch, storeID string) (*ToolingStore, error) {
    70  	cfg := store.DefaultConfig()
    71  	cfg.Architecture = arch
    72  	cfg.StoreID = storeID
    73  	creds, err := getAuthorizer()
    74  	if err != nil {
    75  		return nil, err
    76  	}
    77  	cfg.Authorizer = creds
    78  	if storeURL := os.Getenv("UBUNTU_STORE_URL"); storeURL != "" {
    79  		u, err := url.Parse(storeURL)
    80  		if err != nil {
    81  			return nil, fmt.Errorf("invalid UBUNTU_STORE_URL: %v", err)
    82  		}
    83  		cfg.StoreBaseURL = u
    84  	}
    85  	sto := store.New(cfg, nil)
    86  	return &ToolingStore{
    87  		sto: sto,
    88  		cfg: cfg,
    89  	}, nil
    90  }
    91  
    92  // NewToolingStoreFromModel creates ToolingStore for the snap store used by the given model.
    93  func NewToolingStoreFromModel(model *asserts.Model, fallbackArchitecture string) (*ToolingStore, error) {
    94  	architecture := model.Architecture()
    95  	// can happen on classic
    96  	if architecture == "" {
    97  		architecture = fallbackArchitecture
    98  	}
    99  	return newToolingStore(architecture, model.Store())
   100  }
   101  
   102  // NewToolingStore creates ToolingStore, with optional arch and store id
   103  // read from UBUNTU_STORE_ARCH and UBUNTU_STORE_ID environment variables.
   104  func NewToolingStore() (*ToolingStore, error) {
   105  	arch := os.Getenv("UBUNTU_STORE_ARCH")
   106  	storeID := os.Getenv("UBUNTU_STORE_ID")
   107  	return newToolingStore(arch, storeID)
   108  }
   109  
   110  // DownloadSnapOptions carries options for downloading snaps plus assertions.
   111  type DownloadSnapOptions struct {
   112  	TargetDir string
   113  
   114  	Revision  snap.Revision
   115  	Channel   string
   116  	CohortKey string
   117  	Basename  string
   118  
   119  	LeavePartialOnError bool
   120  }
   121  
   122  var (
   123  	errRevisionAndCohort = errors.New("cannot specify both revision and cohort")
   124  	errPathInBase        = errors.New("cannot specify a path in basename (use target dir for that)")
   125  )
   126  
   127  func (opts *DownloadSnapOptions) validate() error {
   128  	if strings.ContainsRune(opts.Basename, filepath.Separator) {
   129  		return errPathInBase
   130  	}
   131  	if !(opts.Revision.Unset() || opts.CohortKey == "") {
   132  		return errRevisionAndCohort
   133  	}
   134  	return nil
   135  }
   136  
   137  func (opts *DownloadSnapOptions) String() string {
   138  	spec := make([]string, 0, 5)
   139  	if !opts.Revision.Unset() {
   140  		spec = append(spec, fmt.Sprintf("(%s)", opts.Revision))
   141  	}
   142  	if opts.Channel != "" {
   143  		spec = append(spec, fmt.Sprintf("from channel %q", opts.Channel))
   144  	}
   145  	if opts.CohortKey != "" {
   146  		// cohort keys are really long, and the rightmost bit being the
   147  		// interesting bit, so ellipt the rest
   148  		spec = append(spec, fmt.Sprintf(`from cohort %q`, strutil.ElliptLeft(opts.CohortKey, 10)))
   149  	}
   150  	if opts.Basename != "" {
   151  		spec = append(spec, fmt.Sprintf("to %q", opts.Basename+".snap"))
   152  	}
   153  	if opts.TargetDir != "" {
   154  		spec = append(spec, fmt.Sprintf("in %q", opts.TargetDir))
   155  	}
   156  	return strings.Join(spec, " ")
   157  }
   158  
   159  type DownloadedSnap struct {
   160  	Path            string
   161  	Info            *snap.Info
   162  	RedirectChannel string
   163  }
   164  
   165  // DownloadSnap downloads the snap with the given name and options.
   166  // It returns the final full path of the snap and a snap.Info for it and
   167  // optionally a channel the snap got redirected to wrapped in DownloadedSnap.
   168  func (tsto *ToolingStore) DownloadSnap(name string, opts DownloadSnapOptions) (downloadedSnap *DownloadedSnap, err error) {
   169  	if err := opts.validate(); err != nil {
   170  		return nil, err
   171  	}
   172  	sto := tsto.sto
   173  
   174  	if opts.TargetDir == "" {
   175  		pwd, err := os.Getwd()
   176  		if err != nil {
   177  			return nil, err
   178  		}
   179  		opts.TargetDir = pwd
   180  	}
   181  
   182  	if !opts.Revision.Unset() {
   183  		// XXX: is this really necessary (and, if it is, shoudn't we error out instead)
   184  		opts.Channel = ""
   185  	}
   186  
   187  	logger.Debugf("Going to download snap %q %s.", name, &opts)
   188  
   189  	actions := []*store.SnapAction{{
   190  		Action:       "download",
   191  		InstanceName: name,
   192  		Revision:     opts.Revision,
   193  		CohortKey:    opts.CohortKey,
   194  		Channel:      opts.Channel,
   195  	}}
   196  
   197  	sars, _, err := sto.SnapAction(context.TODO(), nil, actions, nil, nil, nil)
   198  	if err != nil {
   199  		// err will be 'cannot download snap "foo": <reasons>'
   200  		return nil, err
   201  	}
   202  	sar := &sars[0]
   203  
   204  	baseName := opts.Basename
   205  	if baseName == "" {
   206  		baseName = sar.Info.Filename()
   207  	} else {
   208  		baseName += ".snap"
   209  	}
   210  	targetFn := filepath.Join(opts.TargetDir, baseName)
   211  
   212  	return tsto.snapDownload(targetFn, sar, opts)
   213  }
   214  
   215  func (tsto *ToolingStore) snapDownload(targetFn string, sar *store.SnapActionResult, opts DownloadSnapOptions) (downloadedSnap *DownloadedSnap, err error) {
   216  	snap := sar.Info
   217  	redirectChannel := sar.RedirectChannel
   218  
   219  	// check if we already have the right file
   220  	if osutil.FileExists(targetFn) {
   221  		sha3_384Dgst, size, err := osutil.FileDigest(targetFn, crypto.SHA3_384)
   222  		if err == nil && size == uint64(snap.DownloadInfo.Size) && fmt.Sprintf("%x", sha3_384Dgst) == snap.DownloadInfo.Sha3_384 {
   223  			logger.Debugf("not downloading, using existing file %s", targetFn)
   224  			return &DownloadedSnap{
   225  				Path:            targetFn,
   226  				Info:            snap,
   227  				RedirectChannel: redirectChannel,
   228  			}, nil
   229  		}
   230  		logger.Debugf("File exists but has wrong hash, ignoring (here).")
   231  	}
   232  
   233  	pb := progress.MakeProgressBar(tsto.Stdout)
   234  	defer pb.Finished()
   235  
   236  	// Intercept sigint
   237  	c := make(chan os.Signal, 3)
   238  	signal.Notify(c, syscall.SIGINT)
   239  	go func() {
   240  		<-c
   241  		pb.Finished()
   242  		os.Exit(1)
   243  	}()
   244  
   245  	dlOpts := &store.DownloadOptions{LeavePartialOnError: opts.LeavePartialOnError}
   246  	if err = tsto.sto.Download(context.TODO(), snap.SnapName(), targetFn, &snap.DownloadInfo, pb, nil, dlOpts); err != nil {
   247  		return nil, err
   248  	}
   249  
   250  	signal.Reset(syscall.SIGINT)
   251  
   252  	return &DownloadedSnap{
   253  		Path:            targetFn,
   254  		Info:            snap,
   255  		RedirectChannel: redirectChannel,
   256  	}, nil
   257  }
   258  
   259  type SnapToDownload struct {
   260  	Snap      naming.SnapRef
   261  	Channel   string
   262  	CohortKey string
   263  }
   264  
   265  type CurrentSnap struct {
   266  	SnapName string
   267  	SnapID   string
   268  	Revision snap.Revision
   269  	Channel  string
   270  	Epoch    snap.Epoch
   271  }
   272  
   273  type DownloadManyOptions struct {
   274  	BeforeDownloadFunc func(*snap.Info) (targetPath string, err error)
   275  	EnforceValidation  bool
   276  }
   277  
   278  // DownloadMany downloads the specified snaps.
   279  // curSnaps are meant to represent already downloaded snaps that will
   280  // be installed in conjunction with the snaps to download, this is needed
   281  // if enforcing validations (ops.EnforceValidation set to true) to
   282  // have cross-gating work.
   283  func (tsto *ToolingStore) DownloadMany(toDownload []SnapToDownload, curSnaps []*CurrentSnap, opts DownloadManyOptions) (downloadedSnaps map[string]*DownloadedSnap, err error) {
   284  	if len(toDownload) == 0 {
   285  		// nothing to do
   286  		return nil, nil
   287  	}
   288  	if opts.BeforeDownloadFunc == nil {
   289  		return nil, fmt.Errorf("internal error: DownloadManyOptions.BeforeDownloadFunc must be set")
   290  	}
   291  
   292  	actionFlag := store.SnapActionIgnoreValidation
   293  	if opts.EnforceValidation {
   294  		actionFlag = store.SnapActionEnforceValidation
   295  	}
   296  
   297  	downloadedSnaps = make(map[string]*DownloadedSnap, len(toDownload))
   298  	current := make([]*store.CurrentSnap, 0, len(curSnaps))
   299  	for _, csnap := range curSnaps {
   300  		ch := "stable"
   301  		if csnap.Channel != "" {
   302  			ch = csnap.Channel
   303  		}
   304  		current = append(current, &store.CurrentSnap{
   305  			InstanceName:     csnap.SnapName,
   306  			SnapID:           csnap.SnapID,
   307  			Revision:         csnap.Revision,
   308  			TrackingChannel:  ch,
   309  			Epoch:            csnap.Epoch,
   310  			IgnoreValidation: !opts.EnforceValidation,
   311  		})
   312  	}
   313  
   314  	actions := make([]*store.SnapAction, 0, len(toDownload))
   315  	for _, sn := range toDownload {
   316  		actions = append(actions, &store.SnapAction{
   317  			Action:       "download",
   318  			InstanceName: sn.Snap.SnapName(), // XXX consider using snap-id first
   319  			Channel:      sn.Channel,
   320  			CohortKey:    sn.CohortKey,
   321  			Flags:        actionFlag,
   322  		})
   323  	}
   324  
   325  	sars, _, err := tsto.sto.SnapAction(context.TODO(), current, actions, nil, nil, nil)
   326  	if err != nil {
   327  		// err will be 'cannot download snap "foo": <reasons>'
   328  		return nil, err
   329  	}
   330  
   331  	for _, sar := range sars {
   332  		targetPath, err := opts.BeforeDownloadFunc(sar.Info)
   333  		if err != nil {
   334  			return nil, err
   335  		}
   336  		dlSnap, err := tsto.snapDownload(targetPath, &sar, DownloadSnapOptions{})
   337  		if err != nil {
   338  			return nil, err
   339  		}
   340  		downloadedSnaps[sar.SnapName()] = dlSnap
   341  	}
   342  
   343  	return downloadedSnaps, nil
   344  }
   345  
   346  // AssertionFetcher creates an asserts.Fetcher for assertions using dlOpts for authorization, the fetcher will add assertions in the given database and after that also call save for each of them.
   347  func (tsto *ToolingStore) AssertionFetcher(db *asserts.Database, save func(asserts.Assertion) error) asserts.Fetcher {
   348  	retrieve := func(ref *asserts.Ref) (asserts.Assertion, error) {
   349  		return tsto.sto.Assertion(ref.Type, ref.PrimaryKey, nil)
   350  	}
   351  	save2 := func(a asserts.Assertion) error {
   352  		// for checking
   353  		err := db.Add(a)
   354  		if err != nil {
   355  			if _, ok := err.(*asserts.RevisionError); ok {
   356  				return nil
   357  			}
   358  			return fmt.Errorf("cannot add assertion %v: %v", a.Ref(), err)
   359  		}
   360  		return save(a)
   361  	}
   362  	return asserts.NewFetcher(db, retrieve, save2)
   363  }
   364  
   365  // Find provides the snapsserts.Finder interface for snapasserts.DerviceSideInfo
   366  func (tsto *ToolingStore) Find(at *asserts.AssertionType, headers map[string]string) (asserts.Assertion, error) {
   367  	pk, err := asserts.PrimaryKeyFromHeaders(at, headers)
   368  	if err != nil {
   369  		return nil, err
   370  	}
   371  	return tsto.sto.Assertion(at, pk, nil)
   372  }
   373  
   374  // MockToolingStore creates a ToolingStore that uses the provided StoreImpl
   375  // implementation for Download, SnapAction and Assertion methods.
   376  // For testing.
   377  func MockToolingStore(sto StoreImpl) *ToolingStore {
   378  	return &ToolingStore{sto: sto}
   379  }