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 }