Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 pkg/drop/attest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ func testResultSet(status string) *papi.ResultSet {

func testAsset() *github.Asset {
return &github.Asset{
Host: "github.com", Org: testOrg, Repo: testAppName,
Host: github.DefaultHost, Org: testOrg, Repo: testAppName,
Version: testVersion, Name: testBinFile, DownloadURL: testAssetURL,
}
}
Expand Down
56 changes: 52 additions & 4 deletions pkg/drop/implementation.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,14 +101,58 @@ func (di *defaultImplementation) GetSystemInfo(*Options) (*system.Info, error) {

// findInstallable looks in a list of release assets for the installable (or
// plain asset) matching the spec name, defaulting to the repository name.
func findInstallable(assets []github.AssetDataProvider, spec github.AssetDataProvider) github.AssetDataProvider {
// findInstallable returns the installable (or plain asset) a spec points to.
// The spec name, defaulting to the repository name, is matched first. When
// the spec carries no name and nothing in the release is named after the
// repository, the fallback is the only installable shipping a variant for
// the platform (releases often name binaries differently than the repo).
// Several such installables are an error listing them, so the user can pick
// one with the #name syntax. Nothing matching returns nil without error.
func findInstallable(assets []github.AssetDataProvider, spec github.AssetDataProvider, osName, arch string) (github.AssetDataProvider, error) {
name := specName(spec)
for _, asset := range assets {
if asset.GetName() == name {
return asset
return asset, nil
}
}
return nil

// An explicit name is never second-guessed
if spec.GetName() != "" {
return nil, nil
}

candidates := []string{}
var found github.AssetDataProvider
for _, asset := range assets {
inst, ok := asset.(*github.Installable)
if !ok || !hasVariant(inst, osName, arch) {
continue
}
candidates = append(candidates, inst.GetName())
found = inst
}
switch len(candidates) {
case 0:
return nil, nil
case 1:
logrus.Debugf("no asset named %q, using the only installable for %s/%s: %s", name, osName, arch, candidates[0])
return found, nil
default:
return nil, fmt.Errorf(
"%w for %s/%s (%s): pick one with %s/%s/%s#<name>", ErrAmbiguousInstallable,
osName, arch, strings.Join(candidates, ", "), spec.GetHost(), spec.GetOrg(), spec.GetRepo(),
)
}
}

// hasVariant reports if an installable ships a variant for a platform.
func hasVariant(inst *github.Installable, osName, arch string) bool {
for _, v := range inst.Variants {
if v.Os == osName && v.Arch == arch {
return true
}
}
return false
}

// ChooseAsset selects an installable matching the spec name and local platform
Expand All @@ -118,7 +162,11 @@ func (di *defaultImplementation) ChooseAsset(opts *GetOptions, client *github.Cl
return nil, fmt.Errorf("fetching release assets: %w", err)
}

if asset := findInstallable(assets, spec); asset != nil {
asset, err := findInstallable(assets, spec, opts.OS, opts.Arch)
if err != nil {
return nil, err
}
if asset != nil {
// Found. Now check if it has variants for the local OS
if installable, ok := asset.(*github.Installable); ok {
var wantedVariant github.AssetDataProvider
Expand Down
6 changes: 5 additions & 1 deletion pkg/drop/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
var (
ErrNoInstallableArtifact = errors.New("release has no binary or compatible package for this platform")
ErrOnlyArchives = errors.New("release only ships archives in unsupported formats for this platform")
ErrAmbiguousInstallable = errors.New("release ships several installables")
)

// ArtifactKind distinguishes the kinds of artifacts the installer can handle.
Expand Down Expand Up @@ -488,7 +489,10 @@ func (di *defaultImplementation) SelectInstallArtifact(
pkgFormat = ""
}

found := findInstallable(assets, spec)
found, err := findInstallable(assets, spec, opts.OS, opts.Arch)
if err != nil {
return nil, err
}
if found == nil {
// Check the variant filenames in case the user pinned an exact file
// in the URL spec:
Expand Down
73 changes: 72 additions & 1 deletion pkg/drop/install_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -593,7 +593,7 @@ func TestRecordInstall(t *testing.T) {
wantDigest := hex.EncodeToString(sum[:])

asset := &github.Asset{
Host: "github.com",
Host: github.DefaultHost,
Org: testOrg,
Repo: testAppName,
Version: "v0.1.0",
Expand Down Expand Up @@ -954,3 +954,74 @@ func TestRemoveInstalledBinary(t *testing.T) {
require.NoError(t, di.RemoveInstalled(opts, &inventory.Record{Name: testAppName, Kind: string(ArtifactBinary)}))
require.Error(t, di.RemoveInstalled(opts, &inventory.Record{Name: testAppName, Kind: "other"}))
}

func TestFindInstallable(t *testing.T) {
t.Parallel()
inst := func(name string, variants ...*github.Asset) *github.Installable {
return &github.Installable{Name: name, Variants: variants}
}
linux := &github.Asset{Name: "x-linux-amd64.tar.gz", Os: system.OSLinux, Arch: system.ArchAMD64}
darwin := &github.Asset{Name: "x-darwin-arm64.tar.gz", Os: system.OSDarwin, Arch: system.ArchArm64}
// A variant whose architecture was not recognized (leaked into the name)
noArch := &github.Asset{Name: "x-2.0-loongarch64-unknown-linux.tar.gz", Os: system.OSLinux}
spec := func(name string) *github.Asset {
return &github.Asset{Host: github.DefaultHost, Org: "atomdrift-project", Repo: "scan", Name: name}
}

for _, tc := range []struct {
name string
assets []github.AssetDataProvider
spec *github.Asset
expect string // installable name, "" = nil
expectErr error
}{
{
name: "named-after-repo",
assets: []github.AssetDataProvider{inst("atomscan", linux), inst("scan", linux)},
spec: spec(""), expect: "scan",
},
{
name: "explicit-name",
assets: []github.AssetDataProvider{inst("atomscan", linux), inst("scan", linux)},
spec: spec("atomscan"), expect: "atomscan",
},
{
name: "explicit-name-never-guessed",
assets: []github.AssetDataProvider{inst("atomscan", linux)},
spec: spec("other"), expect: "",
},
{
name: "fallback-to-only-installable-for-platform",
assets: []github.AssetDataProvider{inst("atomscan", linux, darwin), inst("atomscan-2.0-loongarch64-unknown", noArch), &github.Asset{Name: "SHA256SUMS"}},
spec: spec(""), expect: "atomscan",
},
{
name: "no-installable-for-platform",
assets: []github.AssetDataProvider{inst("atomscan", darwin)},
spec: spec(""), expect: "",
},
{
name: "ambiguous",
assets: []github.AssetDataProvider{inst("atomscan", linux), inst("atomctl", linux)},
spec: spec(""), expectErr: ErrAmbiguousInstallable,
},
} {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
found, err := findInstallable(tc.assets, tc.spec, system.OSLinux, system.ArchAMD64)
if tc.expectErr != nil {
require.ErrorIs(t, err, tc.expectErr)
require.ErrorContains(t, err, "atomscan, atomctl")
require.ErrorContains(t, err, "github.com/atomdrift-project/scan#<name>")
return
}
require.NoError(t, err)
if tc.expect == "" {
require.Nil(t, found)
return
}
require.NotNil(t, found)
require.Equal(t, tc.expect, found.GetName())
})
}
}
25 changes: 19 additions & 6 deletions pkg/drop/policies_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"time"

Expand All @@ -26,8 +27,20 @@ func policySetAttestation(id string) string {
`"policies":[{"id":"pass","meta":{"description":"test","version":1},"tenets":[{"id":"t","code":"true"}]}]}}`, id)
}

// fileLocator returns the file:// locator of a local path, in the form
// localPolicyRepository builds for --policy-repo: forward slashes and a
// leading slash before a windows drive letter (file:///C:/...). A bare
// "file://" + path is not a valid URL on windows.
func fileLocator(path string) string {
slashed := filepath.ToSlash(path)
if !strings.HasPrefix(slashed, "/") {
slashed = "/" + slashed
}
return fileScheme + slashed
}

// newPolicyRepo creates a committed git repository holding one policy set
// per directory, identified by the directory name.
// per directory, identified by the directory name, and returns its locator.
func newPolicyRepo(t *testing.T, dirs ...string) string {
t.Helper()
dir := t.TempDir()
Expand All @@ -50,7 +63,7 @@ func newPolicyRepo(t *testing.T, dirs ...string) string {
Author: &object.Signature{Name: "test", Email: "test@example.com", When: time.Now()},
})
require.NoError(t, err)
return dir
return fileLocator(dir)
}

func TestFetchPolicies(t *testing.T) {
Expand All @@ -70,7 +83,7 @@ func TestFetchPolicies(t *testing.T) {
} {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
opts := &Options{Listener: &NoopListener{}, PolicyRepository: "file://" + newPolicyRepo(t, tc.dirs...)}
opts := &Options{Listener: &NoopListener{}, PolicyRepository: newPolicyRepo(t, tc.dirs...)}
asset := &github.Asset{Host: github.DefaultHost, Org: testOrg, Repo: testAppName}

sets, err := (&defaultImplementation{}).FetchPolicies(opts, asset)
Expand All @@ -88,7 +101,7 @@ func TestFetchPoliciesMissingRepo(t *testing.T) {
t.Parallel()
opts := &Options{
Listener: &NoopListener{},
PolicyRepository: "file://" + filepath.Join(t.TempDir(), "missing"),
PolicyRepository: fileLocator(filepath.Join(t.TempDir(), "missing")),
}
asset := &github.Asset{Host: github.DefaultHost, Org: testOrg, Repo: testAppName}
sets, err := (&defaultImplementation{}).FetchPolicies(opts, asset)
Expand Down Expand Up @@ -119,13 +132,13 @@ func TestFetchPoliciesCommunityFallback(t *testing.T) {
if tc.community {
communityDirs = append(communityDirs, communityDir)
}
orgRepo := "file://" + newPolicyRepo(t, tc.orgDirs...)
orgRepo := newPolicyRepo(t, tc.orgDirs...)
di := &defaultImplementation{
policyRepository: func(_, _ string) string { return orgRepo },
}
opts := &Options{
Listener: &NoopListener{},
CommunityPolicyRepository: "file://" + newPolicyRepo(t, communityDirs...),
CommunityPolicyRepository: newPolicyRepo(t, communityDirs...),
}
if tc.override {
opts.PolicyRepository = orgRepo
Expand Down
41 changes: 41 additions & 0 deletions pkg/github/elements_installable_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -233,3 +233,44 @@ func TestAssetListToInstallableList(t *testing.T) {
})
}
}

// TestRustTargetTriples checks that assets named after Rust target triples
// (arch first, then vendor and OS) are grouped into a single installable,
// including the architectures drop does not install on.
func TestRustTargetTriples(t *testing.T) {
t.Parallel()
files := []string{
"atomscan-2.11.0-aarch64-apple-darwin.tar.gz",
"atomscan-2.11.0-aarch64-unknown-linux-gnu.tar.gz",
"atomscan-2.11.0-loongarch64-unknown-linux-musl.tar.gz",
"atomscan-2.11.0-powerpc64le-unknown-linux-gnu.tar.gz",
"atomscan-2.11.0-riscv64gc-unknown-linux-gnu.tar.gz",
"atomscan-2.11.0-s390x-unknown-linux-gnu.tar.gz",
"atomscan-2.11.0-x86_64-pc-windows-msvc.tar.gz",
"atomscan-2.11.0-x86_64-unknown-linux-gnu.tar.gz",
"SHA256SUMS",
}
expectArch := map[string]string{
files[0]: system.ArchArm64, files[1]: system.ArchArm64, files[2]: system.ArchLoong64,
files[3]: system.ArchPPC64LE, files[4]: system.ArchRiscV64, files[5]: system.ArchS390X,
files[6]: system.ArchX8664, files[7]: system.ArchX8664, files[8]: "",
}
assets := make([]AssetDataProvider, 0, len(files))
for _, f := range files {
require.Equal(t, expectArch[f], getArchFromFilename(f), f)
assets = append(assets, &Asset{Name: f, Version: "v2.11.0"})
}

list := assetListToInstallableList(assets)
require.Len(t, list, 2, "one installable plus the checksums file")
names := make([]string, 0, len(list))
for _, a := range list {
names = append(names, a.GetName())
}
require.ElementsMatch(t, []string{"atomscan", "SHA256SUMS"}, names)
for _, a := range list {
if inst, ok := a.(*Installable); ok {
require.Len(t, inst.Variants, 8)
}
}
}
12 changes: 10 additions & 2 deletions pkg/system/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,10 @@ var ArchAliases = map[string]LabelList{
ArchArm64: {ArchArm64, ArchAarch64},
ArchArm: {ArchArm, ArchArmHF, ArchArmV7, ArchArmV7HL},
Arch386: {Arch386, ArchI686, ArchI386, Arch32Bit},
ArchRiscV64: {ArchRiscV64},
ArchRiscV64: {ArchRiscV64, ArchRiscV64GC},
ArchS390X: {ArchS390X},
ArchPPC64LE: {ArchPPC64LE, ArchPPC64EL, ArchPPC64},
ArchPPC64LE: {ArchPPC64LE, ArchPPC64EL, ArchPPC64, ArchPowerPC64LE, ArchPowerPC64},
ArchLoong64: {ArchLoong64, ArchLoongArch64},
}

// Platform constants
Expand Down Expand Up @@ -64,6 +65,13 @@ const (
ArchPPC64LE = "ppc64le" // IBM Power (redhat naming)
ArchPPC64EL = "ppc64el" // IBM Power (debian naming)
ArchPPC64 = "ppc64"
ArchLoong64 = "loong64" // LoongArch (go naming)

// Rust target triple names
ArchPowerPC64LE = "powerpc64le"
ArchPowerPC64 = "powerpc64"
ArchLoongArch64 = "loongarch64"
ArchRiscV64GC = "riscv64gc"

// Aliases
ArchArmHF = "armhf"
Expand Down
2 changes: 1 addition & 1 deletion pkg/system/system_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (

func TestMainSplitPattern(t *testing.T) {
s := MainSplitPattern()
require.Equal(t, "(?i)(aarch64|armv7hl|freebsd|illumos|openbsd|ppc64el|ppc64le|riscv64|solaris|windows|darwin|netbsd|x86_64|32bit|64bit|amd64|arm64|armhf|armv7|linux|macos|ppc64|s390x|i386|i686|386|arm|osx|x64|x86)", s)
require.Equal(t, "(?i)(loongarch64|powerpc64le|powerpc64|riscv64gc|aarch64|armv7hl|freebsd|illumos|loong64|openbsd|ppc64el|ppc64le|riscv64|solaris|windows|darwin|netbsd|x86_64|32bit|64bit|amd64|arm64|armhf|armv7|linux|macos|ppc64|s390x|i386|i686|386|arm|osx|x64|x86)", s)
}

func TestParseOSReleaseForFamily(t *testing.T) {
Expand Down
Loading