Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TUF autoupdater] Check out latest #1185

Merged
merged 64 commits into from
May 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
62bad56
Split up update library manager into manager + read-only library
RebeccaMahany May 4, 2023
71b2b64
Merge remote-tracking branch 'upstream/main' into becca/tuf-split-up-…
RebeccaMahany May 4, 2023
524a76b
Test installedVersion
RebeccaMahany May 4, 2023
2ce1763
Test for PathToTargetVersionExecutable
RebeccaMahany May 4, 2023
ad45cd3
Clean up library test
RebeccaMahany May 4, 2023
9b89838
Parse tests
RebeccaMahany May 4, 2023
eb6832e
Put back findRelease for now
RebeccaMahany May 4, 2023
9ac0b92
Test sorting versions
RebeccaMahany May 4, 2023
f5c8076
Find install location for app bundle too
RebeccaMahany May 4, 2023
c19051b
Fix up last test
RebeccaMahany May 4, 2023
6da1be7
t.Parallel
RebeccaMahany May 4, 2023
6b378ed
adjust permissions for cache file
RebeccaMahany May 4, 2023
f9fc513
Make log easier to read
RebeccaMahany May 4, 2023
cad0ca5
Evaluate more potential install locations
RebeccaMahany May 4, 2023
e0a2872
Combine libraries
RebeccaMahany May 8, 2023
3809db8
Remove osquerier dependency from library entirely
RebeccaMahany May 8, 2023
f5cbafd
Combine tests
RebeccaMahany May 8, 2023
b219a98
Merge remote-tracking branch 'upstream/main' into becca/tuf-split-up-…
RebeccaMahany May 8, 2023
c893764
Check out latest from library
RebeccaMahany May 8, 2023
a9293dc
Merge branch 'main' into becca/tuf-split-up-library
RebeccaMahany May 8, 2023
20d1770
Merge remote-tracking branch 'upstream/main' into becca/tuf-split-up-…
RebeccaMahany May 8, 2023
b5df3ab
Parse osqueryd version on Windows
RebeccaMahany May 8, 2023
87772ac
Merge branch 'becca/tuf-split-up-library' into becca/tuf-check-out-la…
RebeccaMahany May 8, 2023
a4b6235
Tests for MostRecentVersion
RebeccaMahany May 8, 2023
d4dad51
Move TUF test setup to new package
RebeccaMahany May 8, 2023
dd76b1c
Always return path
RebeccaMahany May 8, 2023
d129505
Merge branch 'becca/tuf-ci' into becca/tuf-check-out-latest
RebeccaMahany May 8, 2023
8f74879
Merge remote-tracking branch 'upstream/main' into becca/tuf-split-up-…
RebeccaMahany May 8, 2023
4f9911e
Merge remote-tracking branch 'upstream/main' into becca/tuf-check-out…
RebeccaMahany May 8, 2023
801c8d5
Log when we can't find the install location
RebeccaMahany May 9, 2023
9f3190d
Merge branch 'becca/tuf-split-up-library' into becca/tuf-check-out-la…
RebeccaMahany May 9, 2023
9ab3e00
Fix test setup
RebeccaMahany May 9, 2023
9db5b65
Add brief docs on library lookup
RebeccaMahany May 9, 2023
ddeadd8
Run subtests in parallel
RebeccaMahany May 9, 2023
6c8e0ec
Fix finding install location in current directory on Windows
RebeccaMahany May 9, 2023
468c1ef
Merge branch 'becca/tuf-split-up-library' into becca/tuf-check-out-la…
RebeccaMahany May 9, 2023
0ea438d
Fix other tests
RebeccaMahany May 9, 2023
cffdffd
Merge remote-tracking branch 'upstream/main' into becca/tuf-split-up-…
RebeccaMahany May 10, 2023
49cf4d0
Remove install version check
RebeccaMahany May 10, 2023
f97a204
Add more useful test data to Test_sortedVersionsInLibrary
RebeccaMahany May 10, 2023
413c0e2
Remove unused var
RebeccaMahany May 10, 2023
6bbe8d7
Merge branch 'becca/tuf-split-up-library' into becca/tuf-check-out-la…
RebeccaMahany May 10, 2023
c660769
Update docs
RebeccaMahany May 10, 2023
5a47a02
Increase timeout on download
RebeccaMahany May 10, 2023
f207ff2
Add another test
RebeccaMahany May 10, 2023
c369898
Move retry for osquery query
RebeccaMahany May 10, 2023
72db950
Don't download current running version
RebeccaMahany May 10, 2023
5bab51d
Merge branch 'becca/tuf-split-up-library' into becca/tuf-check-out-la…
RebeccaMahany May 10, 2023
e9d349d
Return version alongside path, so we won't restart launcher on new pa…
RebeccaMahany May 10, 2023
8cbf752
Merge remote-tracking branch 'upstream/main' into becca/tuf-check-out…
RebeccaMahany May 10, 2023
89e62db
Merge remote-tracking branch 'upstream/main' into becca/tuf-split-up-…
RebeccaMahany May 10, 2023
e4755bc
Do version check earlier to avoid thinking download has been performe…
RebeccaMahany May 10, 2023
e8c83e3
Merge branch 'becca/tuf-split-up-library' into becca/tuf-check-out-la…
RebeccaMahany May 10, 2023
5183fdf
Improve test -- confirm files are created
RebeccaMahany May 12, 2023
9e6f090
Merge remote-tracking branch 'upstream/main' into becca/tuf-check-out…
RebeccaMahany May 12, 2023
9444477
Fix comment
RebeccaMahany May 12, 2023
708f4b3
Make library lookup more lightweight
RebeccaMahany May 16, 2023
0ae45a1
Merge remote-tracking branch 'upstream/main' into becca/tuf-check-out…
RebeccaMahany May 16, 2023
b612e81
Add VersionForChannel for use in launcher doctor, launcher packaging
RebeccaMahany May 18, 2023
c309b25
Pass in logger to capture TUF repo error
RebeccaMahany May 19, 2023
d00570e
Return struct instead of two strings
RebeccaMahany May 19, 2023
6672bcb
Return struct instead of two strings
RebeccaMahany May 19, 2023
42346e9
Rename function for clarity, add documentation
RebeccaMahany May 19, 2023
529b1ce
Rename struct
RebeccaMahany May 19, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/launcher/launcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ func runLauncher(ctx context.Context, cancel func(), opts *launcher.Options) err
metadataClient := http.DefaultClient
metadataClient.Timeout = 1 * time.Minute
mirrorClient := http.DefaultClient
mirrorClient.Timeout = 5 * time.Minute // gives us extra time to avoid a timeout on download
mirrorClient.Timeout = 8 * time.Minute // gives us extra time to avoid a timeout on download
tufAutoupdater, err := tuf.NewTufAutoupdater(
k,
metadataClient,
Expand Down
23 changes: 23 additions & 0 deletions docs/architecture/library_lookup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
## Library lookup

When launcher looks for the version to run for itself or for osquery, it first
looks through local TUF metadata to see if it knows what version to run for its
given release channel. If it does, and the version is already downloaded, it
will run that version.

Otherwise, it will look for the most recent version downloaded to its update
library.

```mermaid
flowchart TB
A[Library lookup] --> B{Do we have a local TUF repo?}
B ---->|No| C[Get most recent version from update library]
C --> D[Return path to most recent version of executable]
D --> H[End]
B -->|Yes| E{release.json target metadata exists?}
E -->|No| C
E --> |Yes| F{Target indicated by release.json\nis downloaded to update library?}
F --> |Yes| G[Return path to selected executable in update library]
F --> |No| C
G --> H
```
15 changes: 8 additions & 7 deletions pkg/autoupdate/tuf/autoupdate.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"fmt"
"net/http"
"os"
"path"
"path/filepath"
"runtime"
"strconv"
Expand Down Expand Up @@ -102,7 +103,7 @@ func NewTufAutoupdater(k types.Knapsack, metadataHttpClient *http.Client, mirror
// If the update directory wasn't set by a flag, use the default location of <launcher root>/updates.
updateDirectory := k.UpdateDirectory()
if updateDirectory == "" {
updateDirectory = DefaultLibraryDirectory(k.RootDirectory())
updateDirectory = defaultLibraryDirectory(k.RootDirectory())
}
ta.libraryManager, err = newUpdateLibraryManager(k.MirrorServerURL(), mirrorHttpClient, updateDirectory, ta.logger)
if err != nil {
Expand Down Expand Up @@ -148,7 +149,7 @@ func LocalTufDirectory(rootDirectory string) string {
return filepath.Join(rootDirectory, tufDirectoryName)
}

func DefaultLibraryDirectory(rootDirectory string) string {
func defaultLibraryDirectory(rootDirectory string) string {
return filepath.Join(rootDirectory, "updates")
}

Expand Down Expand Up @@ -296,7 +297,7 @@ func (ta *TufAutoupdater) checkForUpdate() error {
// downloadUpdate will download a new release for the given binary, if available from TUF
// and not already downloaded.
func (ta *TufAutoupdater) downloadUpdate(binary autoupdatableBinary, targets data.TargetFiles) (string, error) {
release, releaseMetadata, err := ta.findRelease(binary, targets)
release, releaseMetadata, err := findRelease(binary, targets, ta.channel)
if err != nil {
return "", fmt.Errorf("could not find release: %w", err)
}
Expand All @@ -322,12 +323,12 @@ func (ta *TufAutoupdater) downloadUpdate(binary autoupdatableBinary, targets dat
}

// findRelease checks the latest data from TUF (in `targets`) to see whether a new release
// has been published for our channel. If it has, it returns the target for that release
// has been published for the given channel. If it has, it returns the target for that release
// and its associated metadata.
func (ta *TufAutoupdater) findRelease(binary autoupdatableBinary, targets data.TargetFiles) (string, data.TargetFileMeta, error) {
func findRelease(binary autoupdatableBinary, targets data.TargetFiles, channel string) (string, data.TargetFileMeta, error) {
// First, find the target that the channel release file is pointing to
var releaseTarget string
targetReleaseFile := fmt.Sprintf("%s/%s/%s/release.json", binary, runtime.GOOS, ta.channel)
targetReleaseFile := path.Join(string(binary), runtime.GOOS, channel, "release.json")
for targetName, target := range targets {
if targetName != targetReleaseFile {
continue
Expand Down Expand Up @@ -357,7 +358,7 @@ func (ta *TufAutoupdater) findRelease(binary autoupdatableBinary, targets data.T
return filepath.Base(releaseTarget), target, nil
}

return "", data.TargetFileMeta{}, fmt.Errorf("could not find metadata for release target %s for binary %s", targetReleaseFile, binary)
return "", data.TargetFileMeta{}, fmt.Errorf("could not find metadata for release target %s for binary %s", releaseTarget, binary)
}

// storeError saves errors that occur during the periodic check for updates, so that they
Expand Down
35 changes: 35 additions & 0 deletions pkg/autoupdate/tuf/channel_version.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package tuf

import (
"fmt"
"net/http"

"github.com/kolide/launcher/pkg/agent"
)

// GetChannelVersionFromTufServer returns the tagged version of the given binary for the given release channel.
// It is intended for use in e.g. packaging and `launcher doctor`, where we want to determine the correct
// version tag for the channel but do not necessarily have access to a local TUF repo.
func GetChannelVersionFromTufServer(binary, channel, tufServerUrl string) (string, error) {
tempTufRepoDir, err := agent.MkdirTemp("temp-tuf")
if err != nil {
return "", fmt.Errorf("could not make temporary directory: %w", err)
}

tempTufClient, err := initMetadataClient(tempTufRepoDir, tufServerUrl, http.DefaultClient)
if err != nil {
return "", fmt.Errorf("could not init metadata client: %w", err)
}

targets, err := tempTufClient.Update()
if err != nil {
return "", fmt.Errorf("could not update targets: %w", err)
}

releaseTarget, _, err := findRelease(autoupdatableBinary(binary), targets, channel)
if err != nil {
return "", fmt.Errorf("could not find release: %w", err)
}

return versionFromTarget(autoupdatableBinary(binary), releaseTarget), nil
}
90 changes: 90 additions & 0 deletions pkg/autoupdate/tuf/library_lookup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package tuf

import (
"context"
"errors"
"fmt"
"path/filepath"

"github.com/go-kit/kit/log"
"github.com/go-kit/kit/log/level"
"github.com/kolide/launcher/pkg/autoupdate"
)

type BinaryUpdateInfo struct {
Path string
Version string
}

// CheckOutLatest returns the path to the latest downloaded executable for our binary, as well
// as its version.
func CheckOutLatest(binary autoupdatableBinary, rootDirectory string, updateDirectory string, channel string, logger log.Logger) (*BinaryUpdateInfo, error) {
if updateDirectory == "" {
updateDirectory = defaultLibraryDirectory(rootDirectory)
}

update, err := findExecutableFromRelease(binary, LocalTufDirectory(rootDirectory), channel, updateDirectory)
if err == nil {
return update, nil
}

level.Debug(logger).Log("msg", "could not find executable from release", "err", err)

// If we can't find the specific release version that we should be on, then just return the executable
// with the most recent version in the library
return mostRecentVersion(binary, updateDirectory)
}

// findExecutableFromRelease looks at our local TUF repository to find the release for our
// given channel. If it's already downloaded, then we return its path and version.
func findExecutableFromRelease(binary autoupdatableBinary, tufRepositoryLocation string, channel string, baseUpdateDirectory string) (*BinaryUpdateInfo, error) {
// Initialize a read-only TUF metadata client to parse the data we already have downloaded about releases.
metadataClient, err := readOnlyTufMetadataClient(tufRepositoryLocation)
if err != nil {
return nil, errors.New("could not initialize TUF client, cannot find release")
}

// From already-downloaded metadata, look for the release version
targets, err := metadataClient.Targets()
if err != nil {
return nil, fmt.Errorf("could not get target: %w", err)
}

targetName, _, err := findRelease(binary, targets, channel)
if err != nil {
return nil, fmt.Errorf("could not find release: %w", err)
}

targetPath, targetVersion := pathToTargetVersionExecutable(binary, targetName, baseUpdateDirectory)
if autoupdate.CheckExecutable(context.TODO(), targetPath, "--version") != nil {
return nil, fmt.Errorf("version %s from target %s either not yet downloaded or corrupted: %w", targetVersion, targetName, err)
}

return &BinaryUpdateInfo{
Path: targetPath,
Version: targetVersion,
}, nil
}

// mostRecentVersion returns the path to the most recent, valid version available in the library for the
// given binary, along with its version.
func mostRecentVersion(binary autoupdatableBinary, baseUpdateDirectory string) (*BinaryUpdateInfo, error) {
// Pull all available versions from library
validVersionsInLibrary, _, err := sortedVersionsInLibrary(binary, baseUpdateDirectory)
if err != nil {
return nil, fmt.Errorf("could not get sorted versions in library for %s: %w", binary, err)
}

// No valid versions in the library
if len(validVersionsInLibrary) < 1 {
return nil, errors.New("no versions in library")
}

// Versions are sorted in ascending order -- return the last one
mostRecentVersionInLibraryRaw := validVersionsInLibrary[len(validVersionsInLibrary)-1]
versionDir := filepath.Join(updatesDirectory(binary, baseUpdateDirectory), mostRecentVersionInLibraryRaw)
return &BinaryUpdateInfo{
Path: executableLocation(versionDir, binary),
Version: mostRecentVersionInLibraryRaw,
}, nil
}
163 changes: 163 additions & 0 deletions pkg/autoupdate/tuf/library_lookup_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package tuf

import (
"fmt"
"os"
"path/filepath"
"testing"

"github.com/go-kit/kit/log"
tufci "github.com/kolide/launcher/pkg/autoupdate/tuf/ci"
"github.com/stretchr/testify/require"
)

func TestCheckOutLatest_withTufRepository(t *testing.T) {
t.Parallel()

for _, binary := range binaries {
binary := binary
t.Run(string(binary), func(t *testing.T) {
t.Parallel()

// Set up an update library
rootDir := t.TempDir()
updateDir := defaultLibraryDirectory(rootDir)

// Set up a local TUF repo
tufDir := LocalTufDirectory(rootDir)
require.NoError(t, os.MkdirAll(tufDir, 488))
testReleaseVersion := "1.0.30"
expectedTargetName := fmt.Sprintf("%s-%s.tar.gz", binary, testReleaseVersion)
tufci.SeedLocalTufRepo(t, testReleaseVersion, rootDir)

// Create a corresponding downloaded target
executablePath, executableVersion := pathToTargetVersionExecutable(binary, expectedTargetName, updateDir)
require.NoError(t, os.MkdirAll(filepath.Dir(executablePath), 0755))
tufci.CopyBinary(t, executablePath)
require.NoError(t, os.Chmod(executablePath, 0755))

// Make a more recent version that we should ignore since it isn't the release version
tooRecentTarget := fmt.Sprintf("%s-2.1.1.tar.gz", binary)
tooRecentPath, _ := pathToTargetVersionExecutable(binary, tooRecentTarget, updateDir)
require.NoError(t, os.MkdirAll(filepath.Dir(tooRecentPath), 0755))
tufci.CopyBinary(t, tooRecentPath)
require.NoError(t, os.Chmod(tooRecentPath, 0755))

// Check it
latest, err := CheckOutLatest(binary, rootDir, "", "stable", log.NewNopLogger())
require.NoError(t, err, "unexpected error on checking out latest")
require.Equal(t, executablePath, latest.Path)
require.Equal(t, executableVersion, latest.Version)
})
}
}

func TestCheckOutLatest_withoutTufRepository(t *testing.T) {
t.Parallel()

for _, binary := range binaries {
binary := binary
t.Run(string(binary), func(t *testing.T) {
t.Parallel()

// Set up an update library, but no TUF repo
rootDir := t.TempDir()
updateDir := defaultLibraryDirectory(rootDir)
target := fmt.Sprintf("%s-1.1.1.tar.gz", binary)
executablePath, executableVersion := pathToTargetVersionExecutable(binary, target, updateDir)
require.NoError(t, os.MkdirAll(filepath.Dir(executablePath), 0755))
tufci.CopyBinary(t, executablePath)
require.NoError(t, os.Chmod(executablePath, 0755))
_, err := os.Stat(executablePath)
require.NoError(t, err, "did not make test binary")

// Check it
latest, err := CheckOutLatest(binary, rootDir, "", "stable", log.NewNopLogger())
require.NoError(t, err, "unexpected error on checking out latest")
require.Equal(t, executablePath, latest.Path)
require.Equal(t, executableVersion, latest.Version)
})
}
}

func Test_mostRecentVersion(t *testing.T) {
t.Parallel()

for _, binary := range binaries {
binary := binary
t.Run(string(binary), func(t *testing.T) {
t.Parallel()

// Create update directories
testBaseDir := t.TempDir()

// Now, create a version in the update library
firstVersionTarget := fmt.Sprintf("%s-2.2.3.tar.gz", binary)
firstVersionPath, _ := pathToTargetVersionExecutable(binary, firstVersionTarget, testBaseDir)
require.NoError(t, os.MkdirAll(filepath.Dir(firstVersionPath), 0755))
tufci.CopyBinary(t, firstVersionPath)
require.NoError(t, os.Chmod(firstVersionPath, 0755))

// Create an even newer version in the update library
secondVersionTarget := fmt.Sprintf("%s-2.5.3.tar.gz", binary)
secondVersionPath, secondVersion := pathToTargetVersionExecutable(binary, secondVersionTarget, testBaseDir)
require.NoError(t, os.MkdirAll(filepath.Dir(secondVersionPath), 0755))
tufci.CopyBinary(t, secondVersionPath)
require.NoError(t, os.Chmod(secondVersionPath, 0755))

latest, err := mostRecentVersion(binary, testBaseDir)
require.NoError(t, err, "did not expect error getting most recent version")
require.Equal(t, secondVersionPath, latest.Path)
require.Equal(t, secondVersion, latest.Version)
})
}
}

func Test_mostRecentVersion_DoesNotReturnInvalidExecutables(t *testing.T) {
t.Parallel()

for _, binary := range binaries {
binary := binary
t.Run(string(binary), func(t *testing.T) {
t.Parallel()

// Create update directories
testBaseDir := t.TempDir()

// Now, create a version in the update library
firstVersionTarget := fmt.Sprintf("%s-2.2.3.tar.gz", binary)
firstVersionPath, firstVersion := pathToTargetVersionExecutable(binary, firstVersionTarget, testBaseDir)
require.NoError(t, os.MkdirAll(filepath.Dir(firstVersionPath), 0755))
tufci.CopyBinary(t, firstVersionPath)
require.NoError(t, os.Chmod(firstVersionPath, 0755))

// Create an even newer, but also corrupt, version in the update library
secondVersionTarget := fmt.Sprintf("%s-2.1.12.tar.gz", binary)
secondVersionPath, _ := pathToTargetVersionExecutable(binary, secondVersionTarget, testBaseDir)
require.NoError(t, os.MkdirAll(filepath.Dir(secondVersionPath), 0755))
os.WriteFile(secondVersionPath, []byte{}, 0755)

latest, err := mostRecentVersion(binary, testBaseDir)
require.NoError(t, err, "did not expect error getting most recent version")
require.Equal(t, firstVersionPath, latest.Path)
require.Equal(t, firstVersion, latest.Version)
})
}
}

func Test_mostRecentVersion_ReturnsErrorOnNoUpdatesDownloaded(t *testing.T) {
t.Parallel()

for _, binary := range binaries {
binary := binary
t.Run(string(binary), func(t *testing.T) {
t.Parallel()

// Create update directories
testBaseDir := t.TempDir()

_, err := mostRecentVersion(binary, testBaseDir)
require.Error(t, err, "should have returned error when there are no available updates")
})
}
}
Loading