Skip to content

Commit

Permalink
offline-update: Add wave target pull support
Browse files Browse the repository at this point in the history
- 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 <[email protected]>
  • Loading branch information
mike-sul committed Feb 5, 2024
1 parent ff54ca1 commit 3810845
Show file tree
Hide file tree
Showing 2 changed files with 116 additions and 41 deletions.
4 changes: 3 additions & 1 deletion client/foundries.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
153 changes: 113 additions & 40 deletions subcommands/targets/offline-update.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -35,15 +37,19 @@ var (
ouTufOnly bool
ouNoApps bool
ouAllowMultipleTargets bool
ouWave string
)

func init() {
offlineUpdateCmd := &cobra.Command{
Use: "offline-update <target-name> <dst> --tag <tag> [--prod] [--expires-in-days <days>] [--tuf-only]",
Use: "offline-update <target-name> <dst> --tag <tag> [--prod | --wave <wave-name>] [--expires-in-days <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
Expand All @@ -65,25 +71,54 @@ func init() {
"Skip fetching Target Apps")
offlineUpdateCmd.Flags().BoolVarP(&ouAllowMultipleTargets, "allow-multiple-targets", "", false,
"Allow multiple targets to be stored in the same <dst> directory")
offlineUpdateCmd.Flags().StringVarP(&ouWave, "wave", "", "",
"Name of the wave Target content of which to fetch")
}

func doOfflineUpdate(cmd *cobra.Command, args []string) {
factory := viper.GetString("factory")
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 {
Expand All @@ -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/<factory>/targets/<target-name>` 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 {
Expand All @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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 {
Expand Down

0 comments on commit 3810845

Please sign in to comment.