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 7, 2024
1 parent 02a007a commit c51982c
Show file tree
Hide file tree
Showing 2 changed files with 97 additions and 45 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
138 changes: 94 additions & 44 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 @@ -57,6 +63,8 @@ func init() {
"Target tag")
offlineUpdateCmd.Flags().BoolVarP(&ouProd, "prod", "", false,
"Instruct to fetch content of production Target")
offlineUpdateCmd.Flags().StringVarP(&ouWave, "wave", "", "",
"Instruct to fetch content of wave Target; a wave name should be specified")
offlineUpdateCmd.Flags().IntVarP(&ouExpiresIn, "expires-in-days", "e", 30,
"Desired metadata validity period in days")
offlineUpdateCmd.Flags().BoolVarP(&ouTufOnly, "tuf-only", "m", false,
Expand All @@ -65,25 +73,43 @@ 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.MarkFlagsMutuallyExclusive("tag", "wave")
offlineUpdateCmd.MarkFlagsMutuallyExclusive("prod", "wave")
}

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("Either `--tag` or `--wave` should be specified"))
}

fmt.Printf("Checking whether Target exists; target: %s, tag: %s, production: %v\n", targetName, ouTag, ouProd)
subcommands.DieNotNil(checkIfTargetExists(factory, targetName, ouTag, ouProd))

ti, err := getTargetInfo(factory, targetName)
subcommands.DieNotNil(err, "Failed to obtain Target's details:")
var targetCustomData *tuf.FileMeta
var targetGetErr error
// Get the wave/prod/CI specific target with the specified tag to check if it is really present
if len(ouWave) > 0 {
fmt.Printf("Getting Wave Target details; target: %s, wave: %s...\n", targetName, ouWave)
_, targetGetErr = getWaveTargetMeta(factory, targetName, ouWave)
} else if ouProd {
fmt.Printf("Getting production Target details; target: %s, tag: %s...\n", targetName, ouTag)
_, targetGetErr = getProdTargetMeta(factory, targetName, ouTag)
} else {
fmt.Printf("Getting CI Target details; target: %s, tag: %s...\n", targetName, ouTag)
_, targetGetErr = getCiTargetMeta(factory, targetName, ouTag)
}
subcommands.DieNotNil(targetGetErr)
// 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.
targetCustomData, targetGetErr = api.TargetGet(factory, targetName)
subcommands.DieNotNil(targetGetErr)
// Get the target info in order to deduce the ostree and app download URLs
ti, err := getTargetInfo(targetCustomData)
subcommands.DieNotNil(err)

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 +136,17 @@ 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)
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 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) {
func getTargetInfo(targetFile *tuf.FileMeta) (*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)
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 +169,59 @@ 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())
}
if foundTargetMeta, ok := waveTargets[wave].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("No production targets were found for the specified tag `%s`", 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("No CI targets were found for the specified tag `%s`", 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 +233,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 +269,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 +313,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 c51982c

Please sign in to comment.