Skip to content
Open
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
10 changes: 7 additions & 3 deletions client_methods.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,9 @@ func (c *Client) Download(ctx context.Context, pkg PkgInfo) (io.ReadCloser, erro
}

// Install installs a driver with the given name to the specified configuration.
func (c *Client) Install(ctx context.Context, cfg config.Config, driverName string) (*config.Manifest, error) {
// platform selects the driver package platform tuple; when empty, the host
// platform is used.
func (c *Client) Install(ctx context.Context, cfg config.Config, driverName string, platform config.Platform) (*config.Manifest, error) {
drivers, err := c.Search(ctx, driverName)
// Only fail if the driver wasn't found in any registry; partial registry errors
// are acceptable as long as we can still locate the target driver.
Expand All @@ -267,7 +269,9 @@ func (c *Client) Install(ctx context.Context, cfg config.Config, driverName stri
return nil, fmt.Errorf("driver %q not found", driverName)
}

pkg, err := found.GetPackage(nil, config.PlatformTuple(), false)
targetPlatform := platform.Resolve()

pkg, err := found.GetPackage(nil, targetPlatform, false)
if err != nil {
return nil, fmt.Errorf("failed to get package for driver %s: %w", driverName, err)
}
Expand All @@ -278,7 +282,7 @@ func (c *Client) Install(ctx context.Context, cfg config.Config, driverName stri
}
defer os.RemoveAll(filepath.Dir(f.Name()))

manifest, err := config.InstallDriver(cfg, driverName, f)
manifest, err := config.InstallDriver(cfg, driverName, f, targetPlatform)
if err != nil {
return nil, fmt.Errorf("failed to install driver %s: %w", driverName, err)
}
Expand Down
16 changes: 13 additions & 3 deletions client_methods_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,15 +119,25 @@ func TestClientInstall(t *testing.T) {
}

t.Run("installs driver successfully", func(t *testing.T) {
manifest, err := c.Install(t.Context(), cfg, "test-driver-1")
manifest, err := c.Install(t.Context(), cfg, "test-driver-1", "")
require.NoError(t, err)
require.NotNil(t, manifest)
assert.Equal(t, "test-driver-1", manifest.DriverInfo.ID)
assert.NotNil(t, manifest.DriverInfo.Version)
})

t.Run("installs driver for explicit platform", func(t *testing.T) {
const platform = "linux_amd64"
manifest, err := c.Install(t.Context(), cfg, "test-driver-1", config.Platform(platform))
require.NoError(t, err)
require.NotNil(t, manifest)
sharedPath := manifest.DriverInfo.Driver.Shared.Get(platform)
assert.NotEmpty(t, sharedPath)
assert.FileExists(t, sharedPath)
})

t.Run("returns error for nonexistent driver", func(t *testing.T) {
_, err := c.Install(t.Context(), cfg, "nonexistent-driver")
_, err := c.Install(t.Context(), cfg, "nonexistent-driver", "")
assert.Error(t, err)
assert.Contains(t, err.Error(), "nonexistent-driver")
})
Expand All @@ -144,7 +154,7 @@ func TestClientUninstall(t *testing.T) {
Location: tmpDir,
}

_, err := c.Install(t.Context(), cfg, "test-driver-1")
_, err := c.Install(t.Context(), cfg, "test-driver-1", "")
require.NoError(t, err)

manifestPath := filepath.Join(tmpDir, "test-driver-1.toml")
Expand Down
27 changes: 15 additions & 12 deletions cmd/dbc/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ type InstallCmd struct {
// URI url.URL `arg:"-u" placeholder:"URL" help:"Base URL for fetching drivers"`
Driver string `arg:"positional,required" help:"Driver to install, optionally with a version constraint (for example: mysql, mysql=0.1.0, mysql>=1,<2)"`
Level config.ConfigLevel `arg:"-l" help:"Config level to install to (user, system)"`
Platform config.Platform `arg:"--platform" placeholder:"TUPLE" help:"Platform tuple to install for (e.g. linux_amd64). Defaults to the host platform."`
Json bool `arg:"--json" help:"Print output as JSON instead of plaintext"`
JsonStreamProgress bool `arg:"--json-stream-progress" help:"Stream progress events as JSON lines (implies --json)"`
NoVerify bool `arg:"--no-verify" help:"Allow installation of drivers without a signature file"`
Expand All @@ -89,6 +90,7 @@ func (c InstallCmd) GetModelCustom(baseModel baseModel) tea.Model {
}
return progressiveInstallModel{
Driver: c.Driver,
platform: c.Platform.Resolve(),
NoVerify: c.NoVerify,
jsonOutput: c.Json || c.JsonStreamProgress,
jsonStreamProgress: c.JsonStreamProgress,
Expand All @@ -111,12 +113,12 @@ func (c InstallCmd) GetModel() tea.Model {
return c.GetModelCustom(defaultBaseModel())
}

func verifySignature(m config.Manifest, noVerify bool) error {
func verifySignature(m config.Manifest, platform string, noVerify bool) error {
if m.Files.Driver == "" || noVerify {
return nil
}

path := filepath.Dir(m.Driver.Shared.Get(config.PlatformTuple()))
path := filepath.Dir(m.Driver.Shared.Get(platform))

lib, err := os.Open(filepath.Join(path, m.Files.Driver))
if err != nil {
Expand Down Expand Up @@ -215,6 +217,7 @@ type progressiveInstallModel struct {
baseModel

Driver string
platform string
VersionInput *semver.Version
NoVerify bool
jsonOutput bool
Expand Down Expand Up @@ -353,8 +356,8 @@ func (m progressiveInstallModel) FinalOutput() string {
installStatus.Message = m.postInstallMessage
}

if !m.insecureNoChecksum && m.installedDriverInfo.Driver.Shared.Get(config.PlatformTuple()) != "" {
driverPath := m.installedDriverInfo.Driver.Shared.Get(config.PlatformTuple())
if !m.insecureNoChecksum && m.installedDriverInfo.Driver.Shared.Get(m.platform) != "" {
driverPath := m.installedDriverInfo.Driver.Shared.Get(m.platform)
chksum, err := checksum(driverPath)
if err != nil && m.jsonOutput {
return marshalEnvelope("error", jsonschema.ErrorResponse{
Expand Down Expand Up @@ -415,14 +418,14 @@ func (m progressiveInstallModel) searchForDriver(list []dbc.Driver) (tea.Model,
return m, func() tea.Msg {
if vers != nil {
vers.IncludePrerelease = m.Pre
pkg, err := d.GetWithConstraint(vers, config.PlatformTuple())
pkg, err := d.GetWithConstraint(vers, m.platform)
if err != nil {
return err
}
return pkg
}

pkg, err := d.GetPackage(nil, config.PlatformTuple(), m.Pre)
pkg, err := d.GetPackage(nil, m.platform, m.Pre)
if err != nil {
if !m.Pre && !d.HasNonPrerelease() {
for _, cfg := range config.Get() {
Expand All @@ -442,8 +445,8 @@ func (m progressiveInstallModel) startDownloading() (tea.Model, tea.Cmd) {
m.state = stDownloading
if m.isAlreadyInstalled() {
m.state = stDone
if m.jsonOutput && !m.insecureNoChecksum && m.conflictingInfo.Driver.Shared.Get(config.PlatformTuple()) != "" {
driverPath := m.conflictingInfo.Driver.Shared.Get(config.PlatformTuple())
if m.jsonOutput && !m.insecureNoChecksum && m.conflictingInfo.Driver.Shared.Get(m.platform) != "" {
driverPath := m.conflictingInfo.Driver.Shared.Get(m.platform)
return m, func() tea.Msg {
chksum, err := checksum(driverPath)
if err != nil {
Expand All @@ -470,7 +473,7 @@ func (m progressiveInstallModel) startInstalling(downloaded *os.File) (tea.Model
if m.isLocal {
driverName := strings.TrimSuffix(
strings.TrimSuffix(filepath.Base(m.Driver), ".tar.gz"), ".tgz")
parts := strings.Split(driverName, "_"+config.PlatformTuple()+"_")
parts := strings.Split(driverName, "_"+m.platform+"_")
if len(parts) < 2 {
m.Driver = driverName
} else {
Expand All @@ -485,7 +488,7 @@ func (m progressiveInstallModel) startInstalling(downloaded *os.File) (tea.Model
}
}

manifest, err := config.InstallDriver(m.cfg, m.Driver, downloaded)
manifest, err := config.InstallDriver(m.cfg, m.Driver, downloaded, m.platform)
if err != nil {
return err
}
Expand Down Expand Up @@ -557,8 +560,8 @@ func (m progressiveInstallModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m = m.addEvent("extract.complete")
m = m.addEvent("verify.start")
return m, func() tea.Msg {
if err := verifySignature(msg, m.NoVerify); err != nil {
path := filepath.Dir(msg.Driver.Shared.Get(config.PlatformTuple()))
if err := verifySignature(msg, m.platform, m.NoVerify); err != nil {
path := filepath.Dir(msg.Driver.Shared.Get(m.platform))
_ = os.RemoveAll(path)
return err
}
Expand Down
23 changes: 23 additions & 0 deletions cmd/dbc/install_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -629,3 +629,26 @@ func (suite *SubcommandTestSuite) TestInstallJSON_AlreadyInstalledChecksumFailur
suite.Require().NoError(json.Unmarshal(errEnv.Payload, &errPayload))
suite.Equal("install_failed", errPayload.Code, "expected install_failed error code")
}

func (suite *SubcommandTestSuite) TestInstallWithPlatform() {
const platform = "linux_amd64"
m := InstallCmd{
Driver: "test-driver-1",
Level: suite.configLevel,
Platform: config.Platform(platform),
}.GetModelCustom(testBaseModel())
out := suite.runCmd(m)

suite.validateOutput(
"\r[✓] searching\r\n[✓] downloading\r\n[✓] installing\r\n[✓] verifying signature\r\n",
"\nInstalled test-driver-1 1.1.0 to "+suite.Dir(), out)

driver := suite.getInstalledDriver("test-driver-1")
sharedPath := driver.Driver.Shared.Get(platform)
suite.NotEmpty(sharedPath, "manifest should record the requested platform tuple")
suite.FileExists(sharedPath)
if platform != config.PlatformTuple() {
suite.Empty(driver.Driver.Shared.Get(config.PlatformTuple()),
"manifest should not use the host platform tuple when --platform is set")
}
}
106 changes: 84 additions & 22 deletions cmd/dbc/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ import (
)

type ListCmd struct {
Level config.ConfigLevel `arg:"-l" help:"Only list drivers installed at this config level (user, system)"`
Json bool `arg:"--json" help:"Print output as JSON instead of plaintext"`
Level config.ConfigLevel `arg:"-l" help:"Only list drivers installed at this config level (user, system)"`
AllPlatforms bool `arg:"--all-platforms" help:"List drivers for all platforms, including those not loadable on this host"`
Json bool `arg:"--json" help:"Print output as JSON instead of plaintext"`
}

func (ListCmd) Description() string {
Expand All @@ -39,32 +40,64 @@ func (ListCmd) Description() string {

func (c ListCmd) GetModel() tea.Model {
return listModel{
level: c.Level,
jsonOutput: c.Json,
level: c.Level,
allPlatforms: c.AllPlatforms,
jsonOutput: c.Json,
}
}

type installedDriver struct {
Level config.ConfigLevel
ID string
Name string
Version string
Path string
Level config.ConfigLevel
ID string
Name string
Version string
Path string
Platform string
}

type installedDriversMsg []installedDriver

type listModel struct {
baseModel

level config.ConfigLevel
jsonOutput bool
drivers []installedDriver
level config.ConfigLevel
allPlatforms bool
jsonOutput bool
drivers []installedDriver
}

func driverAvailableOnHost(d config.DriverInfo, hostPlatform string) bool {
return d.Driver.Shared.HasPlatform(hostPlatform)
}

func formatPlatformsColumn(d config.DriverInfo, hostPlatform string) string {
// If the driver is configured with a default path (i.e. no explicit
// `<platform> = '/path/to/driver.so'` in the manifest),
// then we assume that the driver is a match for the host platform.
if d.Driver.Shared.UsesDefaultPath() {
return hostPlatform + " (*)"
}

platforms := d.Driver.Shared.PlatformTuples()
if len(platforms) == 0 {
return ""
}

parts := make([]string, len(platforms))
for i, platform := range platforms {
if platform == hostPlatform {
parts[i] = platform + " (*)"
} else {
parts[i] = platform
}
}
return strings.Join(parts, ", ")
}

func (m listModel) Init() tea.Cmd {
return func() tea.Msg {
cfgs := config.Get()
hostPlatform := config.PlatformTuple()

var levels []config.ConfigLevel
if m.level == config.ConfigUnknown {
Expand All @@ -83,17 +116,26 @@ func (m listModel) Init() tea.Cmd {
return fmt.Errorf("failed to list drivers at %s level: %w", lvl, cfg.Err)
}
for _, d := range cfg.Drivers {
if !m.allPlatforms && !driverAvailableOnHost(d, hostPlatform) {
continue
}

version := ""
if d.Version != nil {
version = d.Version.String()
}
drivers = append(drivers, installedDriver{

entry := installedDriver{
Level: lvl,
ID: d.ID,
Name: d.Name,
Version: version,
Path: d.FilePath,
})
}
if m.allPlatforms {
entry.Platform = formatPlatformsColumn(d, hostPlatform)
}
drivers = append(drivers, entry)
}
}

Expand Down Expand Up @@ -136,20 +178,25 @@ func (m listModel) FinalOutput() string {
}

if m.jsonOutput {
return listDriversJSON(m.drivers)
return listDriversJSON(m.drivers, m.allPlatforms)
}
return formatInstalledDrivers(m.drivers)
return formatInstalledDrivers(m.drivers, m.allPlatforms)
}

func formatInstalledDrivers(drivers []installedDriver) string {
func formatInstalledDrivers(drivers []installedDriver, allPlatforms bool) string {
if len(drivers) == 0 {
lipgloss.Fprintln(os.Stderr, "No drivers installed.")
return ""
}

headers := []string{"DRIVER", "VERSION", "LEVEL", "LOCATION"}
if allPlatforms {
headers = []string{"DRIVER", "VERSION", "PLATFORM", "LEVEL", "LOCATION"}
}

t := table.New().Border(lipgloss.HiddenBorder()).
BorderTop(false).BorderBottom(false).BorderLeft(false).BorderRight(false).
Headers("DRIVER", "VERSION", "LEVEL", "LOCATION")
Headers(headers...)
headerStyle := lipgloss.NewStyle().Bold(true)
levelStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("63"))
versionStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("42"))
Expand All @@ -163,27 +210,42 @@ func formatInstalledDrivers(drivers []installedDriver) string {
case 1:
return versionStyle
case 2:
if allPlatforms {
return lipgloss.NewStyle()
}
return levelStyle
case 3:
if allPlatforms {
return levelStyle
}
}
return lipgloss.NewStyle()
})
for _, d := range drivers {
t.Row(d.ID, d.Version, d.Level.String(), d.Path)
if allPlatforms {
t.Row(d.ID, d.Version, d.Platform, d.Level.String(), d.Path)
} else {
t.Row(d.ID, d.Version, d.Level.String(), d.Path)
}
}

return strings.TrimRight(t.String(), "\n")
}

func listDriversJSON(drivers []installedDriver) string {
func listDriversJSON(drivers []installedDriver, allPlatforms bool) string {
entries := make([]jsonschema.ListDriverEntry, 0, len(drivers))
for _, d := range drivers {
entries = append(entries, jsonschema.ListDriverEntry{
entry := jsonschema.ListDriverEntry{
Driver: d.ID,
Name: d.Name,
Version: d.Version,
Level: d.Level.String(),
Location: d.Path,
})
}
if allPlatforms {
entry.Platform = d.Platform
}
entries = append(entries, entry)
}
payloadBytes, err := json.Marshal(jsonschema.ListResponse{Drivers: entries})
if err != nil {
Expand Down
Loading