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
8 changes: 8 additions & 0 deletions pkg/drop/dropper.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
"os"
"path/filepath"

"github.com/sirupsen/logrus"

"github.com/carabiner-dev/drop/pkg/github"
)

Expand Down Expand Up @@ -183,5 +185,11 @@ func (dropper *Dropper) Install(spec github.AssetDataProvider, funcs ...FuncGetO
return fmt.Errorf("installing asset: %w", err)
}

// Register the installation in the inventory. The app is already
// installed at this point, so a recording failure is not fatal.
if err := dropper.impl.RecordInstall(&opts, artifact, downloadPath, !opts.SkipVerification); err != nil {
logrus.Warnf("app installed, but recording it in the inventory failed: %v", err)
}

return nil
}
8 changes: 8 additions & 0 deletions pkg/drop/implementation.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,18 @@ type installerImplementation interface {
// InstallAsset invokes the system mechanism to set up the downloaded artifact
// in the local machine.
InstallAsset(*GetOptions, *system.Info, *InstallArtifact, string) error

// RecordInstall registers a successful installation in the user's
// inventory database so it can later be verified, updated or removed.
RecordInstall(*GetOptions, *InstallArtifact, string, bool) error
}

type defaultImplementation struct {
runner commandRunner

// inventoryPath overrides the location of the inventory database,
// when empty the default (in the user's config dir) is used.
inventoryPath string
}

func (di *defaultImplementation) GetSystemInfo(*Options) (*system.Info, error) {
Expand Down
67 changes: 67 additions & 0 deletions pkg/drop/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ package drop

import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"io"
Expand All @@ -15,6 +17,7 @@ import (
"strings"

"github.com/carabiner-dev/drop/pkg/github"
"github.com/carabiner-dev/drop/pkg/inventory"
"github.com/carabiner-dev/drop/pkg/system"
)

Expand Down Expand Up @@ -492,6 +495,70 @@ func (di *defaultImplementation) installBinary(
return nil
}

// fileDigest returns the hex-encoded sha256 hash of a file.
func fileDigest(path string) (string, error) {
f, err := os.Open(path) //nolint:gosec
if err != nil {
return "", fmt.Errorf("opening file to hash: %w", err)
}
defer f.Close() //nolint:errcheck

h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
return "", fmt.Errorf("hashing file: %w", err)
}
return hex.EncodeToString(h.Sum(nil)), nil
}

// RecordInstall registers a successful installation in the user's inventory
// database so it can later be verified, updated or removed.
func (di *defaultImplementation) RecordInstall(
opts *GetOptions, artifact *InstallArtifact, downloadPath string, verified bool,
) error {
var inv *inventory.Inventory
var err error
if di.inventoryPath == "" {
inv, err = inventory.Open()
} else {
inv, err = inventory.OpenFile(di.inventoryPath)
}
if err != nil {
return fmt.Errorf("opening install inventory: %w", err)
}

// Hash the verified artifact. For binaries this is the same content
// that landed in the binaries directory.
digest, err := fileDigest(downloadPath)
if err != nil {
return err
}

record := &inventory.Record{
Host: artifact.Asset.GetHost(),
Org: artifact.Asset.GetOrg(),
Repo: artifact.Asset.GetRepo(),
Name: strings.TrimSuffix(artifact.InstallName, exeSuffix),
Version: artifact.Asset.GetVersion(),
Kind: string(artifact.Kind),
Asset: artifact.Asset.GetName(),
Digest: map[string]string{"sha256": digest},
Verified: verified,
}

switch artifact.Kind {
case ArtifactBinary:
record.BinPath = filepath.Join(opts.BinDir, artifact.InstallName)
case ArtifactPackage:
record.PackageFormat = artifact.PackageFormat
}

inv.Add(record)
if err := inv.Save(); err != nil {
return fmt.Errorf("saving install inventory: %w", err)
}
return nil
}

// installPackage installs the downloaded package using the system's package
// manager, through sudo when not running as root.
func (di *defaultImplementation) installPackage(
Expand Down
80 changes: 80 additions & 0 deletions pkg/drop/install_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
package drop

import (
"crypto/sha256"
"encoding/hex"
"errors"
"net/http"
"net/http/httptest"
Expand All @@ -15,6 +17,7 @@ import (
"github.com/stretchr/testify/require"

"github.com/carabiner-dev/drop/pkg/github"
"github.com/carabiner-dev/drop/pkg/inventory"
"github.com/carabiner-dev/drop/pkg/system"
)

Expand Down Expand Up @@ -423,3 +426,80 @@ func TestDownloadAssetToTmp(t *testing.T) {
require.NoError(t, err)
require.Equal(t, "artifact-data", string(data))
}

func TestRecordInstall(t *testing.T) {
t.Parallel()
content := []byte("artifact-data")
sum := sha256.Sum256(content)
wantDigest := hex.EncodeToString(sum[:])

asset := &github.Asset{
Host: "github.com",
Org: "carabiner-dev",
Repo: testAppName,
Version: "v0.1.0",
Name: testBinFile,
Os: system.OSLinux,
Arch: system.ArchAMD64,
}

for _, tc := range []struct {
name string
artifact *InstallArtifact
verified bool
check func(t *testing.T, r *inventory.Record)
}{
{
name: "binary",
artifact: &InstallArtifact{
Kind: ArtifactBinary, Asset: asset, InstallName: testAppName,
},
verified: true,
check: func(t *testing.T, r *inventory.Record) {
t.Helper()
require.Equal(t, string(ArtifactBinary), r.Kind)
require.Equal(t, filepath.Join("/opt/bin", testAppName), r.BinPath)
require.Empty(t, r.PackageFormat)
require.True(t, r.Verified)
},
},
{
name: "package-unverified",
artifact: &InstallArtifact{
Kind: ArtifactPackage, PackageFormat: system.PackageRPM,
Asset: asset, InstallName: testAppName,
},
verified: false,
check: func(t *testing.T, r *inventory.Record) {
t.Helper()
require.Equal(t, string(ArtifactPackage), r.Kind)
require.Equal(t, system.PackageRPM, r.PackageFormat)
require.Empty(t, r.BinPath)
require.False(t, r.Verified)
},
},
} {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
downloaded := filepath.Join(t.TempDir(), testBinFile)
require.NoError(t, os.WriteFile(downloaded, content, 0o600))

invPath := filepath.Join(t.TempDir(), "installed.json")
di := &defaultImplementation{inventoryPath: invPath}
opts := &GetOptions{BinDir: "/opt/bin"}

require.NoError(t, di.RecordInstall(opts, tc.artifact, downloaded, tc.verified))

inv, err := inventory.OpenFile(invPath)
require.NoError(t, err)
record := inv.Get("github.com/carabiner-dev/drop#drop")
require.NotNil(t, record)
require.Equal(t, testAppName, record.Name)
require.Equal(t, "v0.1.0", record.Version)
require.Equal(t, testBinFile, record.Asset)
require.Equal(t, map[string]string{"sha256": wantDigest}, record.Digest)
require.False(t, record.InstalledAt.IsZero())
tc.check(t, record)
})
}
}
Loading
Loading