diff --git a/cmd/dbc/add.go b/cmd/dbc/add.go index 26d71919..ff8bfd5a 100644 --- a/cmd/dbc/add.go +++ b/cmd/dbc/add.go @@ -94,10 +94,14 @@ func (m addModel) Init() tea.Cmd { } return func() tea.Msg { - drivers, err := m.getDriverRegistry() - if err != nil { - return fmt.Errorf("error getting driver list: %w", err) + drivers, registryErr := m.getDriverRegistry() + // If we have no drivers and there's an error, fail immediately + if len(drivers) == 0 && registryErr != nil { + return fmt.Errorf("error getting driver list: %w", registryErr) } + // Store registry errors to use later if driver is not found + // We continue processing if we have some drivers + var registryErrors error = registryErr p, err := driverListPath(m.Path) if err != nil { @@ -130,6 +134,10 @@ func (m addModel) Init() tea.Cmd { drv, err := findDriver(spec.Name, drivers) if err != nil { + // If we have registry errors, enhance the error message + if registryErrors != nil { + return fmt.Errorf("%w\n\nNote: Some driver registries were unavailable:\n%s", err, registryErrors.Error()) + } return err } @@ -141,7 +149,12 @@ func (m addModel) Init() tea.Cmd { } } else { if !m.Pre && !drv.HasNonPrerelease() { - return fmt.Errorf("driver `%s` not found in driver registry index", spec.Name) + err := fmt.Errorf("driver `%s` not found in driver registry index", spec.Name) + // If we have registry errors, enhance the error message + if registryErrors != nil { + return fmt.Errorf("%w\n\nNote: Some driver registries were unavailable:\n%s", err, registryErrors.Error()) + } + return err } } diff --git a/cmd/dbc/add_test.go b/cmd/dbc/add_test.go index d6790a37..02cd9a18 100644 --- a/cmd/dbc/add_test.go +++ b/cmd/dbc/add_test.go @@ -17,6 +17,7 @@ package main import ( "bytes" "context" + "fmt" "os" "path/filepath" "testing" @@ -326,3 +327,85 @@ func (suite *SubcommandTestSuite) TestAddExplicitPrereleaseWithoutPreFlag() { version = '=0.9.0-alpha.1' `, string(data)) } + +func (suite *SubcommandTestSuite) TestAddPartialRegistryFailure() { + // Initialize driver list + m := InitCmd{Path: filepath.Join(suite.tempdir, "dbc.toml")}.GetModel() + suite.runCmd(m) + + // Test that add command handles partial registry failure gracefully + // (one registry succeeds, another fails - returns both drivers and error) + partialFailingRegistry := func() ([]dbc.Driver, error) { + // Get drivers from the test registry (simulating one successful registry) + drivers, _ := getTestDriverRegistry() + // But also return an error (simulating another registry that failed) + return drivers, fmt.Errorf("registry https://cdn-fallback.example.com: failed to fetch driver registry: DNS resolution failed") + } + + // Should succeed if the requested driver is found in the available drivers + m = AddCmd{ + Path: filepath.Join(suite.tempdir, "dbc.toml"), + Driver: []string{"test-driver-1"}, + Pre: false, + }.GetModelCustom( + baseModel{getDriverRegistry: partialFailingRegistry, downloadPkg: downloadTestPkg}) + + suite.runCmd(m) + // Should succeed without printing the registry error + + // Verify the file was updated correctly + data, err := os.ReadFile(filepath.Join(suite.tempdir, "dbc.toml")) + suite.Require().NoError(err) + suite.Contains(string(data), "[drivers.test-driver-1]") +} + +func (suite *SubcommandTestSuite) TestAddPartialRegistryFailureDriverNotFound() { + // Initialize driver list + m := InitCmd{Path: filepath.Join(suite.tempdir, "dbc.toml")}.GetModel() + suite.runCmd(m) + + // Test that add command shows registry errors when the requested driver is not found + partialFailingRegistry := func() ([]dbc.Driver, error) { + // Get drivers from the test registry (simulating one successful registry) + drivers, _ := getTestDriverRegistry() + // But also return an error (simulating another registry that failed) + return drivers, fmt.Errorf("registry https://cdn-fallback.example.com: failed to fetch driver registry: DNS resolution failed") + } + + // Should fail with enhanced error message if the requested driver is not found + m = AddCmd{ + Path: filepath.Join(suite.tempdir, "dbc.toml"), + Driver: []string{"nonexistent-driver"}, + Pre: false, + }.GetModelCustom( + baseModel{getDriverRegistry: partialFailingRegistry, downloadPkg: downloadTestPkg}) + + out := suite.runCmdErr(m) + // Should show the driver not found error AND the registry error + suite.Contains(out, "driver `nonexistent-driver` not found") + suite.Contains(out, "Note: Some driver registries were unavailable") + suite.Contains(out, "failed to fetch driver registry") + suite.Contains(out, "DNS resolution failed") +} + +func (suite *SubcommandTestSuite) TestAddCompleteRegistryFailure() { + // Initialize driver list + m := InitCmd{Path: filepath.Join(suite.tempdir, "dbc.toml")}.GetModel() + suite.runCmd(m) + + // Test that add command handles complete registry failure (no drivers returned) + completeFailingRegistry := func() ([]dbc.Driver, error) { + return nil, fmt.Errorf("registry https://primary-cdn.example.com: network unreachable") + } + + m = AddCmd{ + Path: filepath.Join(suite.tempdir, "dbc.toml"), + Driver: []string{"test-driver-1"}, + Pre: false, + }.GetModelCustom( + baseModel{getDriverRegistry: completeFailingRegistry, downloadPkg: downloadTestPkg}) + + out := suite.runCmdErr(m) + suite.Contains(out, "error getting driver list") + suite.Contains(out, "network unreachable") +} diff --git a/cmd/dbc/docs.go b/cmd/dbc/docs.go index bad9f8c4..26f3dcec 100644 --- a/cmd/dbc/docs.go +++ b/cmd/dbc/docs.go @@ -67,12 +67,13 @@ func (c DocsCmd) GetModel() tea.Model { type docsModel struct { baseModel - driver string - drv *dbc.Driver - urlToOpen string - noOpen bool - fallbackUrls map[string]string - openBrowser func(string) error + driver string + drv *dbc.Driver + urlToOpen string + noOpen bool + fallbackUrls map[string]string + openBrowser func(string) error + registryErrors error // Store registry errors for better error messages } func (m docsModel) Init() tea.Cmd { @@ -81,13 +82,18 @@ func (m docsModel) Init() tea.Cmd { return docsUrlFound(dbcDocsUrl) } - drivers, err := m.getDriverRegistry() - if err != nil { - return err + drivers, registryErr := m.getDriverRegistry() + // If we have no drivers and there's an error, fail immediately + if len(drivers) == 0 && registryErr != nil { + return fmt.Errorf("error getting driver list: %w", registryErr) } drv, err := findDriver(m.driver, drivers) if err != nil { + // If we have registry errors, enhance the error message + if registryErr != nil { + return fmt.Errorf("%w\n\nNote: Some driver registries were unavailable:\n%s", err, registryErr.Error()) + } return err } diff --git a/cmd/dbc/docs_test.go b/cmd/dbc/docs_test.go index 76b35827..79f368e9 100644 --- a/cmd/dbc/docs_test.go +++ b/cmd/dbc/docs_test.go @@ -16,6 +16,8 @@ package main import ( "fmt" + + "github.com/columnar-tech/dbc" ) var testFallbackUrls = map[string]string{ @@ -154,3 +156,80 @@ func (suite *SubcommandTestSuite) TestDocsDriverFoundWithDocs() { suite.Equal("http://example.com", lastOpenedURL) } + +func (suite *SubcommandTestSuite) TestDocsPartialRegistryFailure() { + // Test that docs command handles partial registry failure gracefully + // (one registry succeeds, another fails - returns both drivers and error) + partialFailingRegistry := func() ([]dbc.Driver, error) { + // Get drivers from the test registry (simulating one successful registry) + drivers, _ := getTestDriverRegistry() + // But also return an error (simulating another registry that failed) + return drivers, fmt.Errorf("registry https://fallback-registry.example.com: failed to fetch driver registry: timeout") + } + + openBrowserFunc = mockOpenBrowserSuccess + lastOpenedURL = "" + fallbackDriverDocsUrl = testFallbackUrls + + // Should succeed if the requested driver is found in the available drivers + m := DocsCmd{Driver: "test-driver-1"}.GetModelCustom( + baseModel{getDriverRegistry: partialFailingRegistry, downloadPkg: downloadTestPkg}, + false, + mockOpenBrowserSuccess, + testFallbackUrls, + ) + + suite.runCmd(m) + // Should open docs successfully without showing the registry error + suite.Equal("https://test.example.com/driver1", lastOpenedURL) +} + +func (suite *SubcommandTestSuite) TestDocsPartialRegistryFailureDriverNotFound() { + // Test that docs command shows registry errors when the requested driver is not found + partialFailingRegistry := func() ([]dbc.Driver, error) { + // Get drivers from the test registry (simulating one successful registry) + drivers, _ := getTestDriverRegistry() + // But also return an error (simulating another registry that failed) + return drivers, fmt.Errorf("registry https://fallback-registry.example.com: failed to fetch driver registry: timeout") + } + + openBrowserFunc = mockOpenBrowserSuccess + lastOpenedURL = "" + + // Should fail with enhanced error message if the requested driver is not found + m := DocsCmd{Driver: "nonexistent-driver"}.GetModelCustom( + baseModel{getDriverRegistry: partialFailingRegistry, downloadPkg: downloadTestPkg}, + false, + mockOpenBrowserSuccess, + testFallbackUrls, + ) + + out := suite.runCmdErr(m) + // Should show the driver not found error AND the registry error + suite.Contains(out, "driver `nonexistent-driver` not found") + suite.Contains(out, "Note: Some driver registries were unavailable") + suite.Contains(out, "failed to fetch driver registry") + suite.Contains(out, "timeout") + suite.Equal("", lastOpenedURL, "browser should not be opened on error") +} + +func (suite *SubcommandTestSuite) TestDocsCompleteRegistryFailure() { + // Test that docs command handles complete registry failure (no drivers returned) + completeFailingRegistry := func() ([]dbc.Driver, error) { + return nil, fmt.Errorf("registry https://main-registry.example.com: connection timeout") + } + + openBrowserFunc = mockOpenBrowserSuccess + lastOpenedURL = "" + + m := DocsCmd{Driver: "test-driver-1"}.GetModelCustom( + baseModel{getDriverRegistry: completeFailingRegistry, downloadPkg: downloadTestPkg}, + false, + mockOpenBrowserSuccess, + testFallbackUrls, + ) + + out := suite.runCmdErr(m) + suite.Contains(out, "connection timeout") + suite.Equal("", lastOpenedURL, "browser should not be opened on error") +} diff --git a/cmd/dbc/info.go b/cmd/dbc/info.go index bdbbff29..8cd4884d 100644 --- a/cmd/dbc/info.go +++ b/cmd/dbc/info.go @@ -16,6 +16,7 @@ package main import ( "encoding/json" + "fmt" "strings" tea "github.com/charmbracelet/bubbletea" @@ -45,20 +46,26 @@ func (c InfoCmd) GetModel() tea.Model { type infoModel struct { baseModel - driver string - jsonOutput bool - drv dbc.Driver + driver string + jsonOutput bool + drv dbc.Driver + registryErrors error // Store registry errors for better error messages } func (m infoModel) Init() tea.Cmd { return func() tea.Msg { - drivers, err := m.getDriverRegistry() - if err != nil { - return err + drivers, registryErr := m.getDriverRegistry() + // If we have no drivers and there's an error, fail immediately + if len(drivers) == 0 && registryErr != nil { + return fmt.Errorf("error getting driver list: %w", registryErr) } drv, err := findDriver(m.driver, drivers) if err != nil { + // If we have registry errors, enhance the error message + if registryErr != nil { + return fmt.Errorf("%w\n\nNote: Some driver registries were unavailable:\n%s", err, registryErr.Error()) + } return err } diff --git a/cmd/dbc/info_test.go b/cmd/dbc/info_test.go index e4137b24..aa4a4bb3 100644 --- a/cmd/dbc/info_test.go +++ b/cmd/dbc/info_test.go @@ -14,6 +14,12 @@ package main +import ( + "fmt" + + "github.com/columnar-tech/dbc" +) + func (suite *SubcommandTestSuite) TestInfo() { m := InfoCmd{Driver: "test-driver-1"}. GetModelCustom(baseModel{getDriverRegistry: getTestDriverRegistry, downloadPkg: downloadTestPkg}) @@ -34,3 +40,57 @@ func (suite *SubcommandTestSuite) TestInfo_DriverNotFound() { suite.validateOutput("\r ", "\nError: driver `non-existent-driver` not found in driver registry index", out) } + +func (suite *SubcommandTestSuite) TestInfoPartialRegistryFailure() { + // Test that info command handles partial registry failure gracefully + // (one registry succeeds, another fails - returns both drivers and error) + partialFailingRegistry := func() ([]dbc.Driver, error) { + // Get drivers from the test registry (simulating one successful registry) + drivers, _ := getTestDriverRegistry() + // But also return an error (simulating another registry that failed) + return drivers, fmt.Errorf("registry https://secondary-registry.example.com: failed to fetch driver registry: DNS error") + } + + // Should succeed if the requested driver is found in the available drivers + m := InfoCmd{Driver: "test-driver-1"}. + GetModelCustom(baseModel{getDriverRegistry: partialFailingRegistry, downloadPkg: downloadTestPkg}) + + out := suite.runCmd(m) + // Should display info successfully without printing the registry error + suite.Contains(out, "Driver: test-driver-1") + suite.Contains(out, "Version: 1.1.0") +} + +func (suite *SubcommandTestSuite) TestInfoPartialRegistryFailureDriverNotFound() { + // Test that info command shows registry errors when the requested driver is not found + partialFailingRegistry := func() ([]dbc.Driver, error) { + // Get drivers from the test registry (simulating one successful registry) + drivers, _ := getTestDriverRegistry() + // But also return an error (simulating another registry that failed) + return drivers, fmt.Errorf("registry https://secondary-registry.example.com: failed to fetch driver registry: DNS error") + } + + // Should fail with enhanced error message if the requested driver is not found + m := InfoCmd{Driver: "nonexistent-driver"}. + GetModelCustom(baseModel{getDriverRegistry: partialFailingRegistry, downloadPkg: downloadTestPkg}) + + out := suite.runCmdErr(m) + // Should show the driver not found error AND the registry error + suite.Contains(out, "driver `nonexistent-driver` not found") + suite.Contains(out, "Note: Some driver registries were unavailable") + suite.Contains(out, "failed to fetch driver registry") + suite.Contains(out, "DNS error") +} + +func (suite *SubcommandTestSuite) TestInfoCompleteRegistryFailure() { + // Test that info command handles complete registry failure (no drivers returned) + completeFailingRegistry := func() ([]dbc.Driver, error) { + return nil, fmt.Errorf("registry https://primary-registry.example.com: network unreachable") + } + + m := InfoCmd{Driver: "test-driver-1"}. + GetModelCustom(baseModel{getDriverRegistry: completeFailingRegistry, downloadPkg: downloadTestPkg}) + + out := suite.runCmdErr(m) + suite.Contains(out, "network unreachable") +} diff --git a/cmd/dbc/install.go b/cmd/dbc/install.go index 20c6dcf7..55debda2 100644 --- a/cmd/dbc/install.go +++ b/cmd/dbc/install.go @@ -168,6 +168,13 @@ type progressiveInstallModel struct { width, height int isLocal bool + + registryErrors error // Store registry errors for better error messages +} + +type driversWithRegistryError struct { + drivers []dbc.Driver + err error } func (m progressiveInstallModel) Init() tea.Cmd { @@ -179,10 +186,12 @@ func (m progressiveInstallModel) Init() tea.Cmd { return tea.Batch(m.spinner.Tick, func() tea.Msg { drivers, err := m.getDriverRegistry() - if err != nil { - return err + // Return both drivers and error - we'll decide how to handle based on whether + // the requested driver is found + return driversWithRegistryError{ + drivers: drivers, + err: err, } - return drivers }) } @@ -252,6 +261,10 @@ func (m progressiveInstallModel) searchForDriver(list []dbc.Driver) (tea.Model, m.Driver = driverName d, err := findDriver(m.Driver, list) if err != nil { + // If we have registry errors, enhance the error message + if m.registryErrors != nil { + return m, errCmd("could not find driver: %w\n\nNote: Some driver registries were unavailable:\n%s", err, m.registryErrors.Error()) + } return m, errCmd("could not find driver: %w", err) } @@ -335,7 +348,11 @@ func (m progressiveInstallModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { p, cmd := m.p.Update(msg) m.p = p.(dbc.FileProgressModel) return m, cmd + case driversWithRegistryError: + m.registryErrors = msg.err + return m.searchForDriver(msg.drivers) case []dbc.Driver: + // For backwards compatibility, still handle plain driver list return m.searchForDriver(msg) case localInstallMsg: m.isLocal = true diff --git a/cmd/dbc/install_test.go b/cmd/dbc/install_test.go index 4ff7a162..c6538c20 100644 --- a/cmd/dbc/install_test.go +++ b/cmd/dbc/install_test.go @@ -15,10 +15,12 @@ package main import ( + "fmt" "os" "path/filepath" "runtime" + "github.com/columnar-tech/dbc" "github.com/columnar-tech/dbc/config" ) @@ -353,3 +355,59 @@ func (suite *SubcommandTestSuite) TestInstallExplicitPrereleaseWithoutPreFlag() "\nInstalled test-driver-only-pre 0.9.0-alpha.1 to "+suite.Dir()+"\n", out) suite.driverIsInstalled("test-driver-only-pre", false) } + +func (suite *SubcommandTestSuite) TestInstallPartialRegistryFailure() { + // Test that install command handles partial registry failure gracefully + // (one registry succeeds, another fails - returns both drivers and error) + partialFailingRegistry := func() ([]dbc.Driver, error) { + // Get drivers from the test registry (simulating one successful registry) + drivers, _ := getTestDriverRegistry() + // But also return an error (simulating another registry that failed) + return drivers, fmt.Errorf("registry https://secondary-registry.example.com: failed to fetch driver registry: network error") + } + + // Should succeed if the requested driver is found in the available drivers + m := InstallCmd{Driver: "test-driver-1", Level: suite.configLevel}. + GetModelCustom(baseModel{getDriverRegistry: partialFailingRegistry, downloadPkg: downloadTestPkg}) + out := suite.runCmd(m) + + // Should install successfully without printing the registry error + suite.Contains(out, "Installed test-driver-1 1.1.0") + suite.driverIsInstalled("test-driver-1", true) +} + +func (suite *SubcommandTestSuite) TestInstallPartialRegistryFailureDriverNotFound() { + // Test that install command shows registry errors when the requested driver is not found + partialFailingRegistry := func() ([]dbc.Driver, error) { + // Get drivers from the test registry (simulating one successful registry) + drivers, _ := getTestDriverRegistry() + // But also return an error (simulating another registry that failed) + return drivers, fmt.Errorf("registry https://secondary-registry.example.com: failed to fetch driver registry: network error") + } + + // Should fail with enhanced error message if the requested driver is not found + m := InstallCmd{Driver: "nonexistent-driver", Level: suite.configLevel}. + GetModelCustom(baseModel{getDriverRegistry: partialFailingRegistry, downloadPkg: downloadTestPkg}) + out := suite.runCmdErr(m) + + // Should show the driver not found error AND the registry error + suite.Contains(out, "could not find driver") + suite.Contains(out, "nonexistent-driver") + suite.Contains(out, "Note: Some driver registries were unavailable") + suite.Contains(out, "failed to fetch driver registry") + suite.Contains(out, "network error") +} + +func (suite *SubcommandTestSuite) TestInstallCompleteRegistryFailure() { + // Test that install command handles complete registry failure (no drivers returned) + completeFailingRegistry := func() ([]dbc.Driver, error) { + return nil, fmt.Errorf("registry https://primary-registry.example.com: connection timeout") + } + + m := InstallCmd{Driver: "test-driver-1", Level: suite.configLevel}. + GetModelCustom(baseModel{getDriverRegistry: completeFailingRegistry, downloadPkg: downloadTestPkg}) + out := suite.runCmdErr(m) + + suite.Contains(out, "connection timeout") + suite.driverIsNotInstalled("test-driver-1") +} diff --git a/cmd/dbc/search.go b/cmd/dbc/search.go index f0548e75..87b1e312 100644 --- a/cmd/dbc/search.go +++ b/cmd/dbc/search.go @@ -63,21 +63,28 @@ func (s SearchCmd) GetModel() tea.Model { type searchModel struct { baseModel - verbose bool - outputJson bool - pre bool - pattern *regexp.Regexp - finalDrivers []dbc.Driver + verbose bool + outputJson bool + pre bool + pattern *regexp.Regexp + finalDrivers []dbc.Driver + registryErrors error // Store registry errors to display as warnings +} + +type driversWithErrorMsg struct { + drivers []dbc.Driver + err error } func (m searchModel) Init() tea.Cmd { return func() tea.Msg { drivers, err := m.getDriverRegistry() - if err != nil { - return err + // Don't fail completely if we have some drivers - return them with the error + // This allows graceful degradation when some registries fail + return driversWithErrorMsg{ + drivers: m.filterDrivers(drivers), + err: err, } - - return m.filterDrivers(drivers) } } @@ -97,7 +104,17 @@ func (m searchModel) filterDrivers(drivers []dbc.Driver) []dbc.Driver { func (m searchModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { + case driversWithErrorMsg: + m.finalDrivers = msg.drivers + m.registryErrors = msg.err + // If we have no drivers and there's an error, fail the command + if len(msg.drivers) == 0 && msg.err != nil { + m.err = msg.err + m.status = 1 + } + return m, tea.Sequence(tea.Quit) case []dbc.Driver: + // For backwards compatibility, still handle plain driver list m.finalDrivers = msg return m, tea.Sequence(tea.Quit) default: @@ -184,7 +201,7 @@ func viewDrivers(d []dbc.Driver, verbose bool, allowPre bool) string { return l.String() } -func viewDriversJSON(d []dbc.Driver, verbose bool, allowPre bool) string { +func viewDriversJSON(d []dbc.Driver, verbose bool, allowPre bool, registryErrors error) string { current := config.Get() if !verbose { @@ -195,14 +212,14 @@ func viewDriversJSON(d []dbc.Driver, verbose bool, allowPre bool) string { Registry string `json:"registry,omitempty"` } - var result []output + var driverList []output for _, driver := range d { installed, _ := getInstalled(driver, current) if !allowPre && !driver.HasNonPrerelease() && len(installed) == 0 { continue } - result = append(result, output{ + driverList = append(driverList, output{ Driver: driver.Path, Description: driver.Desc, Installed: installed, @@ -210,7 +227,17 @@ func viewDriversJSON(d []dbc.Driver, verbose bool, allowPre bool) string { }) } - jsonBytes, err := json.Marshal(result) + type result struct { + Drivers []output `json:"drivers"` + Warning string `json:"warning,omitempty"` + } + + res := result{Drivers: driverList} + if registryErrors != nil && len(d) > 0 { + res.Warning = registryErrors.Error() + } + + jsonBytes, err := json.Marshal(res) if err != nil { return fmt.Sprintf("error marshaling JSON: %v", err) } @@ -226,7 +253,7 @@ func viewDriversJSON(d []dbc.Driver, verbose bool, allowPre bool) string { AvailableVersions []string `json:"available_versions,omitempty"` } - var result []output + var driverList []output for _, driver := range d { _, installedVerbose := getInstalled(driver, current) @@ -239,7 +266,7 @@ func viewDriversJSON(d []dbc.Driver, verbose bool, allowPre bool) string { availableVersions = append(availableVersions, v.String()) } - result = append(result, output{ + driverList = append(driverList, output{ Driver: driver.Path, Description: driver.Desc, License: driver.License, @@ -249,7 +276,17 @@ func viewDriversJSON(d []dbc.Driver, verbose bool, allowPre bool) string { }) } - jsonBytes, err := json.Marshal(result) + type result struct { + Drivers []output `json:"drivers"` + Warning string `json:"warning,omitempty"` + } + + res := result{Drivers: driverList} + if registryErrors != nil && len(d) > 0 { + res.Warning = registryErrors.Error() + } + + jsonBytes, err := json.Marshal(res) if err != nil { return fmt.Sprintf("error marshaling JSON: %v", err) } @@ -271,8 +308,22 @@ func getInstalled(driver dbc.Driver, cfg map[config.ConfigLevel]config.Config) ( } func (m searchModel) FinalOutput() string { + var output string + + // Display driver list first if m.outputJson { - return viewDriversJSON(m.finalDrivers, m.verbose, m.pre) + output = viewDriversJSON(m.finalDrivers, m.verbose, m.pre, m.registryErrors) + } else { + output = viewDrivers(m.finalDrivers, m.verbose, m.pre) + } + + // Display warning about registry errors after the driver list (only if we have some drivers to show) + // If we have no drivers, the error is returned via the error mechanism + if !m.outputJson && m.registryErrors != nil && len(m.finalDrivers) > 0 { + warningStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("214")).Bold(true) + output += "\n" + warningStyle.Render("Warning: ") + "Some driver registries were unavailable:\n" + output += m.registryErrors.Error() + "\n" } - return viewDrivers(m.finalDrivers, m.verbose, m.pre) + + return output } diff --git a/cmd/dbc/search_test.go b/cmd/dbc/search_test.go index 8e8266de..c86b663c 100644 --- a/cmd/dbc/search_test.go +++ b/cmd/dbc/search_test.go @@ -15,10 +15,12 @@ package main import ( + "fmt" "os" "path/filepath" "strings" + "github.com/columnar-tech/dbc" "github.com/columnar-tech/dbc/config" ) @@ -206,3 +208,65 @@ func (suite *SubcommandTestSuite) TestSearchCmdWithInstalledPre() { "test-driver-invalid-manifest This is test driver with an invalid manifest. See https://github.com/columnar-tech/dbc/issues/37. \n"+ "test-driver-docs-url This is manifest-only with its docs_url key set ", suite.runCmd(m)) } + +func (suite *SubcommandTestSuite) TestSearchCmdPartialRegistryFailure() { + // Test that search command handles partial registry failure gracefully + // (one registry succeeds, another fails - returns both drivers and error) + partialFailingRegistry := func() ([]dbc.Driver, error) { + // Get drivers from the test registry (simulating one successful registry) + drivers, _ := getTestDriverRegistry() + // But also return an error (simulating another registry that failed) + return drivers, fmt.Errorf("registry https://backup-registry.example.com: failed to fetch driver registry: connection timeout") + } + + // The search should succeed and display a warning about the failed registry + m := SearchCmd{}.GetModelCustom( + baseModel{getDriverRegistry: partialFailingRegistry, downloadPkg: downloadTestPkg}) + out := suite.runCmd(m) + + // Should show warning about registry failure + suite.Contains(out, "Warning:") + suite.Contains(out, "Some driver registries were unavailable") + suite.Contains(out, "failed to fetch driver registry") + suite.Contains(out, "connection timeout") + + // Should still display drivers from the successful registry + suite.Contains(out, "test-driver-1") + suite.Contains(out, "test-driver-2") +} + +func (suite *SubcommandTestSuite) TestSearchCmdCompleteRegistryFailure() { + // Test that search command handles complete registry failure (no drivers returned) + completeFailingRegistry := func() ([]dbc.Driver, error) { + return nil, fmt.Errorf("registry https://main-registry.example.com: DNS resolution failed") + } + + m := SearchCmd{}.GetModelCustom( + baseModel{getDriverRegistry: completeFailingRegistry, downloadPkg: downloadTestPkg}) + out := suite.runCmdErr(m) + + // Should show the error + suite.Contains(out, "DNS resolution failed") + // Should NOT show the warning when no drivers are available + suite.NotContains(out, "Warning:") + suite.NotContains(out, "Some driver registries were unavailable") +} + +func (suite *SubcommandTestSuite) TestSearchCmdPartialRegistryFailureJSON() { + // Test that JSON search output includes warning about partial registry failure + partialFailingRegistry := func() ([]dbc.Driver, error) { + drivers, _ := getTestDriverRegistry() + return drivers, fmt.Errorf("registry https://backup-registry.example.com: connection timeout") + } + + m := SearchCmd{Json: true}.GetModelCustom( + baseModel{getDriverRegistry: partialFailingRegistry, downloadPkg: downloadTestPkg}) + out := suite.runCmd(m) + + // JSON output should include the warning field + suite.Contains(out, `"warning"`) + suite.Contains(out, "connection timeout") + // Should still have drivers + suite.Contains(out, `"drivers"`) + suite.Contains(out, "test-driver-1") +} diff --git a/cmd/dbc/sync.go b/cmd/dbc/sync.go index 52ec3446..4b4c77a9 100644 --- a/cmd/dbc/sync.go +++ b/cmd/dbc/sync.go @@ -88,7 +88,8 @@ type syncModel struct { progress progress.Model width, height int - done bool + done bool + registryErrors error // Store registry errors for better error messages } type driversListMsg struct { @@ -166,6 +167,10 @@ func (s syncModel) createInstallList(list DriversList) ([]installItem, error) { // locate the driver info in the CDN driver registry index drv, err := findDriver(name, s.driverIndex) if err != nil { + // If we have registry errors, enhance the error message + if s.registryErrors != nil { + return nil, fmt.Errorf("%w\n\nNote: Some driver registries were unavailable:\n%s", err, s.registryErrors.Error()) + } return nil, err } @@ -331,12 +336,29 @@ func (s syncModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { s.list = msg.list return s, func() tea.Msg { drivers, err := s.getDriverRegistry() + // Return both drivers and error - we'll decide how to handle based on whether + // all requested drivers can be found + return driversWithRegistryError{ + drivers: drivers, + err: err, + } + } + case driversWithRegistryError: + s.registryErrors = msg.err + // If we have no drivers and there's an error, fail immediately + if len(msg.drivers) == 0 && msg.err != nil { + return s, errCmd("error getting driver list: %w", msg.err) + } + s.driverIndex = msg.drivers + return s, func() tea.Msg { + items, err := s.createInstallList(s.list) if err != nil { return err } - return drivers + return items } case []dbc.Driver: + // For backwards compatibility, still handle plain driver list s.driverIndex = msg return s, func() tea.Msg { items, err := s.createInstallList(s.list) diff --git a/cmd/dbc/sync_test.go b/cmd/dbc/sync_test.go index 96b116a2..bb33e839 100644 --- a/cmd/dbc/sync_test.go +++ b/cmd/dbc/sync_test.go @@ -15,8 +15,11 @@ package main import ( + "fmt" "os" "path/filepath" + + "github.com/columnar-tech/dbc" ) func (suite *SubcommandTestSuite) TestSync() { @@ -156,3 +159,87 @@ func (suite *SubcommandTestSuite) TestSyncInstallNoVerify() { baseModel{getDriverRegistry: getTestDriverRegistry, downloadPkg: downloadTestPkg}) suite.validateOutput("✓ test-driver-no-sig-1.1.0\r\n\rDone!\r\n", "", suite.runCmd(m)) } + +func (suite *SubcommandTestSuite) TestSyncPartialRegistryFailure() { + // Initialize driver list + m := InitCmd{Path: filepath.Join(suite.tempdir, "dbc.toml")}.GetModel() + suite.runCmd(m) + + m = AddCmd{Path: filepath.Join(suite.tempdir, "dbc.toml"), Driver: []string{"test-driver-1"}}.GetModel() + suite.runCmd(m) + + // Test that sync command handles partial registry failure gracefully + // (one registry succeeds, another fails - returns both drivers and error) + partialFailingRegistry := func() ([]dbc.Driver, error) { + // Get drivers from the test registry (simulating one successful registry) + drivers, _ := getTestDriverRegistry() + // But also return an error (simulating another registry that failed) + return drivers, fmt.Errorf("registry https://backup-registry.example.com: failed to fetch driver registry: network timeout") + } + + // Should succeed if the requested driver is found in the available drivers + m = SyncCmd{ + Path: filepath.Join(suite.tempdir, "dbc.toml"), + }.GetModelCustom( + baseModel{getDriverRegistry: partialFailingRegistry, downloadPkg: downloadTestPkg}) + + // Should install successfully without printing the registry error + suite.validateOutput("✓ test-driver-1-1.1.0\r\n\rDone!\r\n", "", suite.runCmd(m)) + suite.FileExists(filepath.Join(suite.tempdir, "test-driver-1.toml")) +} + +func (suite *SubcommandTestSuite) TestSyncPartialRegistryFailureDriverNotFound() { + // Initialize driver list with a driver that doesn't exist + m := InitCmd{Path: filepath.Join(suite.tempdir, "dbc.toml")}.GetModel() + suite.runCmd(m) + + // Manually create a driver list with a nonexistent driver + err := os.WriteFile(filepath.Join(suite.tempdir, "dbc.toml"), []byte(`# dbc driver list +[drivers] +[drivers.nonexistent-driver] +`), 0644) + suite.Require().NoError(err) + + // Test that sync command shows registry errors when the requested driver is not found + partialFailingRegistry := func() ([]dbc.Driver, error) { + // Get drivers from the test registry (simulating one successful registry) + drivers, _ := getTestDriverRegistry() + // But also return an error (simulating another registry that failed) + return drivers, fmt.Errorf("registry https://backup-registry.example.com: failed to fetch driver registry: network timeout") + } + + // Should fail with enhanced error message if the requested driver is not found + m = SyncCmd{ + Path: filepath.Join(suite.tempdir, "dbc.toml"), + }.GetModelCustom( + baseModel{getDriverRegistry: partialFailingRegistry, downloadPkg: downloadTestPkg}) + + out := suite.runCmdErr(m) + // Should show the driver not found error AND the registry error + suite.Contains(out, "driver `nonexistent-driver` not found") + suite.Contains(out, "Note: Some driver registries were unavailable") + suite.Contains(out, "failed to fetch driver registry") + suite.Contains(out, "network timeout") +} + +func (suite *SubcommandTestSuite) TestSyncCompleteRegistryFailure() { + // Initialize driver list + m := InitCmd{Path: filepath.Join(suite.tempdir, "dbc.toml")}.GetModel() + suite.runCmd(m) + + m = AddCmd{Path: filepath.Join(suite.tempdir, "dbc.toml"), Driver: []string{"test-driver-1"}}.GetModel() + suite.runCmd(m) + + // Test that sync command handles complete registry failure (no drivers returned) + completeFailingRegistry := func() ([]dbc.Driver, error) { + return nil, fmt.Errorf("registry https://primary-registry.example.com: connection refused") + } + + m = SyncCmd{ + Path: filepath.Join(suite.tempdir, "dbc.toml"), + }.GetModelCustom( + baseModel{getDriverRegistry: completeFailingRegistry, downloadPkg: downloadTestPkg}) + + out := suite.runCmdErr(m) + suite.Contains(out, "connection refused") +} diff --git a/drivers.go b/drivers.go index f3280bf2..a256ac68 100644 --- a/drivers.go +++ b/drivers.go @@ -252,17 +252,19 @@ func getDriverListFromIndex(index *Registry) ([]Driver, error) { } var getDrivers = sync.OnceValues(func() ([]Driver, error) { + var totalErr error allDrivers := make([]Driver, 0) for i := range registries { drivers, err := getDriverListFromIndex(®istries[i]) if err != nil { - return nil, err + totalErr = errors.Join(totalErr, fmt.Errorf("registry %s: %w", registries[i].BaseURL, err)) + continue } registries[i].Drivers = drivers allDrivers = append(allDrivers, drivers...) } - return allDrivers, nil + return allDrivers, totalErr }) //go:embed columnar.pubkey