From 3810845d12169ec21a01c376f7776b1d9e5783d4 Mon Sep 17 00:00:00 2001 From: Mike Sul Date: Fri, 2 Feb 2024 10:08:46 +0100 Subject: [PATCH] offline-update: Add wave target pull support - Add ability to download wave's TUF metadata and its target content to the offline update bundle. - Correct checking whether the production target exists for a given tag Signed-off-by: Mike Sul --- client/foundries.go | 4 +- subcommands/targets/offline-update.go | 153 +++++++++++++++++++------- 2 files changed, 116 insertions(+), 41 deletions(-) diff --git a/client/foundries.go b/client/foundries.go index b6b68214..fc3eb430 100644 --- a/client/foundries.go +++ b/client/foundries.go @@ -1159,18 +1159,20 @@ func (a *Api) TufMetadataGet(factory string, metadata string, tag string, prod b return a.Get(url) } -func (a *Api) TufTargetMetadataRefresh(factory string, target string, tag string, expiresIn int, prod bool) (map[string]tuf.Signed, error) { +func (a *Api) TufTargetMetadataRefresh(factory string, target string, tag string, expiresIn int, prod bool, wave string) (map[string]tuf.Signed, error) { url := a.serverUrl + "/ota/factories/" + factory + "/targets/" + target + "/meta/" type targetMeta struct { Tag string `json:"tag"` ExpiresIn int `json:"expires-in-days"` Prod bool `json:"production"` + Wave string `json:"wave"` } b, err := json.Marshal(targetMeta{ Tag: tag, ExpiresIn: expiresIn, Prod: prod, + Wave: wave, }) if err != nil { return nil, err diff --git a/subcommands/targets/offline-update.go b/subcommands/targets/offline-update.go index 21f6b14b..096c7aac 100644 --- a/subcommands/targets/offline-update.go +++ b/subcommands/targets/offline-update.go @@ -13,6 +13,8 @@ import ( "strings" "time" + tuf "github.com/theupdateframework/notary/tuf/data" + "github.com/foundriesio/fioctl/client" "github.com/foundriesio/fioctl/subcommands" "github.com/spf13/cobra" @@ -35,15 +37,19 @@ var ( ouTufOnly bool ouNoApps bool ouAllowMultipleTargets bool + ouWave string ) func init() { offlineUpdateCmd := &cobra.Command{ - Use: "offline-update --tag [--prod] [--expires-in-days ] [--tuf-only]", + Use: "offline-update --tag [--prod | --wave ] [--expires-in-days ] [--tuf-only]", Short: "Download Target content for an offline update", Run: doOfflineUpdate, Args: cobra.ExactArgs(2), Example: ` + # Download update content of the wave target #1451 for "intel-corei7-64" hardware type + fioctl targets offline-update intel-corei7-64-lmp-1451 /mnt/flash-drive/offline-update-content --wave wave-deployment-001 + # Download update content of the production target #1451 tagged by "release-01" for "intel-corei7-64" hardware type fioctl targets offline-update intel-corei7-64-lmp-1451 /mnt/flash-drive/offline-update-content --tag release-01 --prod @@ -65,6 +71,8 @@ func init() { "Skip fetching Target Apps") offlineUpdateCmd.Flags().BoolVarP(&ouAllowMultipleTargets, "allow-multiple-targets", "", false, "Allow multiple targets to be stored in the same directory") + offlineUpdateCmd.Flags().StringVarP(&ouWave, "wave", "", "", + "Name of the wave Target content of which to fetch") } func doOfflineUpdate(cmd *cobra.Command, args []string) { @@ -72,18 +80,45 @@ func doOfflineUpdate(cmd *cobra.Command, args []string) { targetName := args[0] dstDir := args[1] - if len(ouTag) == 0 { - subcommands.DieNotNil(errors.New("missing mandatory flag `--tag`")) + if len(ouTag) == 0 && len(ouWave) == 0 { + subcommands.DieNotNil(errors.New("one of the mandatory parameters `--tag` or `--wave` should be specified")) + } + + if len(ouTag) > 0 && len(ouWave) > 0 { + subcommands.DieNotNil(errors.New("either `--tag` or `--wave` should be specified exclusively")) } - fmt.Printf("Checking whether Target exists; target: %s, tag: %s, production: %v\n", targetName, ouTag, ouProd) - subcommands.DieNotNil(checkIfTargetExists(factory, targetName, ouTag, ouProd)) + if len(ouWave) > 0 && ouProd { + subcommands.DieNotNil(errors.New("`the --prod` flag is redundant if the `--wave` parameter is specified")) + } - ti, err := getTargetInfo(factory, targetName) + var getTargetMeta func() (*tuf.FileMeta, error) + if len(ouWave) > 0 { + getTargetMeta = func() (*tuf.FileMeta, error) { + fmt.Printf("Getting Wave Target details; target: %s, wave: %s...\n", targetName, ouWave) + return getWaveTargetMeta(factory, targetName, ouWave) + } + } else if ouProd { + getTargetMeta = func() (*tuf.FileMeta, error) { + fmt.Printf("Getting production Target details; target: %s, tag: %s...\n", targetName, ouTag) + return getProdTargetMeta(factory, targetName, ouTag) + } + } else { + getTargetMeta = func() (*tuf.FileMeta, error) { + fmt.Printf("Getting CI Target details; target: %s, tag: %s...\n", targetName, ouTag) + return getCiTargetMeta(factory, targetName, ouTag) + } + } + // Get the wave/prod/CI specific target with the specified tag to check if it is really present, and + // get the specified target from the list of factory targets to obtain the "original" tag/branch that produced + // the target so we can find out the correct app bundle fetch URL. + ti, err := checkAndGetTargetInfo(getTargetMeta, func() (*tuf.FileMeta, error) { + return api.TargetGet(factory, targetName) + }) subcommands.DieNotNil(err, "Failed to obtain Target's details:") fmt.Printf("Refreshing and downloading TUF metadata for Target %s to %s...\n", targetName, path.Join(dstDir, "tuf")) - subcommands.DieNotNil(downloadTufRepo(factory, targetName, ouTag, ouProd, ouExpiresIn, path.Join(dstDir, "tuf")), "Failed to download TUF metadata:") + subcommands.DieNotNil(downloadTufRepo(factory, targetName, ouTag, ouProd, ouWave, ouExpiresIn, path.Join(dstDir, "tuf")), "Failed to download TUF metadata:") fmt.Println("Successfully refreshed and downloaded TUF metadata") if !ouTufOnly { @@ -110,37 +145,26 @@ Notice that multiple targets in the same directory is only supported in LmP >= v } } -func checkIfTargetExists(factory string, targetName string, tag string, prod bool) error { - data, err := api.TufMetadataGet(factory, "targets.json", tag, prod) - if err != nil { - if herr := client.AsHttpError(err); herr != nil && herr.Response.StatusCode == 404 { - return fmt.Errorf("the specified Target has not been found; target: %s, tag: %s, production: %v", targetName, ouTag, ouProd) - } - return fmt.Errorf("failed to check whether Target exists: %s", err.Error()) - } - targets := client.AtsTufTargets{} - err = json.Unmarshal(*data, &targets) +func checkAndGetTargetInfo(checkTargetMeta, getTargetMeta func() (*tuf.FileMeta, error)) (*ouTargetInfo, error) { + // Check whether the specified wave/prod/CI target with the given tag exists + _, err := checkTargetMeta() if err != nil { - return fmt.Errorf("failed to check whether Target exists: %s", err.Error()) - } - for tn := range targets.Signed.Targets { - if tn == targetName { - return nil - } + return nil, err } - return fmt.Errorf("the specified Target has not been found; target: %s, tag: %s, production: %v", targetName, ouTag, ouProd) -} - -func getTargetInfo(factory string, targetName string) (*ouTargetInfo, error) { + // Since the wave/prod/CI specific target json usually doesn't contain the "original" branch/tag that the apps were fetched for + // we do the following to determine where to fetch the target app bundle from. // Getting Target's custom info from the `/ota/factories//targets/` because: // 1. a target name is unique and represents the same Target across all "tagged" targets set including prod; // 2. only this target version/representation contains an original tag(s)/branch that // the `image-assemble` and apps fetching was performed for (needed for determining where to download Apps from). - custom, err := getTargetCustomInfo(factory, targetName) + targetFile, err := getTargetMeta() + if err != nil { + return nil, err + } + custom, err := api.TargetCustom(*targetFile) if err != nil { return nil, err } - info := ouTargetInfo{} info.version, err = strconv.Atoi(custom.Version) if err != nil { @@ -163,6 +187,64 @@ func getTargetInfo(factory string, targetName string) (*ouTargetInfo, error) { return &info, nil } +func getWaveTargetMeta(factory string, targetName string, wave string) (*tuf.FileMeta, error) { + waveTargets, err := api.WaveTargetsList(factory, true, wave) + if err != nil { + if herr := client.AsHttpError(err); herr != nil && herr.Response.StatusCode == 404 { + return nil, fmt.Errorf("no active Wave with the specified name was found; wave: %s", wave) + } + return nil, fmt.Errorf("failed to get Wave Target metadata: %s", err.Error()) + } + targets, ok := waveTargets[wave] + if !ok { + // it looks rather like panic since the call at #281 is supposed to return only the given wave targets + subcommands.DieNotNil(fmt.Errorf("no Targets are found for the given wave %s", wave)) + } + if foundTargetMeta, ok := targets.Signed.Targets[targetName]; ok { + return &foundTargetMeta, nil + } else { + return nil, fmt.Errorf("the specified Target is not found among wave targets;"+ + " target: %s, wave: %s", targetName, wave) + } +} + +func getProdTargetMeta(factory string, targetName string, tag string) (*tuf.FileMeta, error) { + targets, err := api.ProdTargetsGet(factory, tag, true) + if err != nil { + if herr := client.AsHttpError(err); herr != nil && herr.Response.StatusCode == 404 { + return nil, fmt.Errorf("the specified production Target has not been found; target: %s, tag: %s", targetName, tag) + } + return nil, fmt.Errorf("failed to get production Target metadata: %s", err.Error()) + } + if foundTargetMeta, ok := targets.Signed.Targets[targetName]; ok { + return &foundTargetMeta, nil + } else { + return nil, fmt.Errorf("no production target with the given tag is found;"+ + " target: %s, tag: %s", targetName, tag) + } +} + +func getCiTargetMeta(factory string, targetName string, tag string) (*tuf.FileMeta, error) { + data, err := api.TufMetadataGet(factory, "targets.json", tag, false) + if err != nil { + if herr := client.AsHttpError(err); herr != nil && herr.Response.StatusCode == 404 { + return nil, fmt.Errorf("the specified CI Target has not been found; target: %s, tag: %s", targetName, tag) + } + return nil, fmt.Errorf("failed to get CI Target metadata: %s", err.Error()) + } + targets := client.AtsTufTargets{} + err = json.Unmarshal(*data, &targets) + if err != nil { + return nil, err + } + if foundTargetMeta, ok := targets.Signed.Targets[targetName]; ok { + return &foundTargetMeta, nil + } else { + return nil, fmt.Errorf("no CI target with the given tag is found;"+ + " target: %s, tag: %s", targetName, tag) + } +} + func isDstDirClean(dstDir string) bool { for _, subDir := range []string{"ostree_repo", "apps"} { fullPath := path.Join(dstDir, subDir) @@ -174,7 +256,7 @@ func isDstDirClean(dstDir string) bool { return true } -func downloadTufRepo(factory string, target string, tag string, prod bool, expiresIn int, dstDir string) error { +func downloadTufRepo(factory string, target string, tag string, prod bool, wave string, expiresIn int, dstDir string) error { // v1 - auto-generated by tuf_keyserver (default, on Factory creation); // v2 - auto-generated by ota-lite to take keys online (default, on Factory creation); ver := 3 @@ -210,7 +292,7 @@ func downloadTufRepo(factory string, target string, tag string, prod bool, expir ver += 1 } - meta, err := api.TufTargetMetadataRefresh(factory, target, tag, expiresIn, prod) + meta, err := api.TufTargetMetadataRefresh(factory, target, tag, expiresIn, prod, wave) if err != nil { return err } @@ -254,15 +336,6 @@ func downloadApps(factory string, targetName string, targetVer int, tag string, }) } -func getTargetCustomInfo(factory string, targetName string) (*client.TufCustom, error) { - targetFile, err := api.TargetGet(factory, targetName) - if err != nil { - return nil, err - } - custom, err := api.TargetCustom(*targetFile) - return custom, err -} - func downloadItem(factory string, targetVer int, runName string, artifactPath string, storeHandler func(r io.Reader) error) error { resp, err := api.JobservRunArtifact(factory, targetVer, runName, artifactPath) if err != nil {