Skip to content

Commit 1d2e445

Browse files
fix: avoid helm conflict with local directory (#99)
1 parent 1825378 commit 1d2e445

File tree

3 files changed

+112
-19
lines changed

3 files changed

+112
-19
lines changed

internal/cmd/local/local/cmd.go

+31-15
Original file line numberDiff line numberDiff line change
@@ -62,18 +62,22 @@ type HTTPClient interface {
6262
// BrowserLauncher primarily for testing purposes.
6363
type BrowserLauncher func(url string) error
6464

65+
// ChartLocator primarily for testing purposes.
66+
type ChartLocator func(repoName, repoUrl string) string
67+
6568
// Command is the local command, responsible for installing, uninstalling, or other local actions.
6669
type Command struct {
67-
provider k8s.Provider
68-
cluster k8s.Cluster
69-
http HTTPClient
70-
helm helm.Client
71-
k8s k8s.Client
72-
portHTTP int
73-
spinner *pterm.SpinnerPrinter
74-
tel telemetry.Client
75-
launcher BrowserLauncher
76-
userHome string
70+
provider k8s.Provider
71+
cluster k8s.Cluster
72+
http HTTPClient
73+
helm helm.Client
74+
k8s k8s.Client
75+
portHTTP int
76+
spinner *pterm.SpinnerPrinter
77+
tel telemetry.Client
78+
launcher BrowserLauncher
79+
locateChart ChartLocator
80+
userHome string
7781
}
7882

7983
// Option for configuring the Command, primarily exists for testing
@@ -114,6 +118,12 @@ func WithBrowserLauncher(launcher BrowserLauncher) Option {
114118
}
115119
}
116120

121+
func WithChartLocator(locator ChartLocator) Option {
122+
return func(c *Command) {
123+
c.locateChart = locator
124+
}
125+
}
126+
117127
// WithUserHome define the user's home directory.
118128
func WithUserHome(home string) Option {
119129
return func(c *Command) {
@@ -140,6 +150,10 @@ func New(provider k8s.Provider, opts ...Option) (*Command, error) {
140150
opt(c)
141151
}
142152

153+
if c.locateChart == nil {
154+
c.locateChart = locateLatestAirbyteChart
155+
}
156+
143157
// determine userhome if not defined
144158
if c.userHome == "" {
145159
c.userHome = paths.UserHome
@@ -698,11 +712,13 @@ func (c *Command) handleChart(
698712
return fmt.Errorf("unable to add %s chart repo: %w", req.name, err)
699713
}
700714

701-
c.spinner.UpdateText(fmt.Sprintf("Fetching %s Helm Chart", req.chartName))
702-
helmChart, _, err := c.helm.GetChart(req.chartName, &action.ChartPathOptions{Version: req.chartVersion})
715+
c.spinner.UpdateText(fmt.Sprintf("Fetching %s Helm Chart with version", req.chartName))
716+
717+
chartLoc := c.locateChart(req.chartName, req.chartVersion)
718+
719+
helmChart, _, err := c.helm.GetChart(chartLoc, &action.ChartPathOptions{Version: req.chartVersion})
703720
if err != nil {
704-
pterm.Error.Printfln("Unable to fetch %s Helm Chart", req.chartName)
705-
return fmt.Errorf("unable to fetch chart %s: %w", req.chartName, err)
721+
return fmt.Errorf("unable to fetch helm chart %q: %w", req.chartName, err)
706722
}
707723

708724
c.tel.Attr(fmt.Sprintf("helm_%s_chart_version", req.name), helmChart.Metadata.Version)
@@ -741,7 +757,7 @@ func (c *Command) handleChart(
741757
))
742758
helmRelease, err := c.helm.InstallOrUpgradeChart(ctx, &helmclient.ChartSpec{
743759
ReleaseName: req.chartRelease,
744-
ChartName: req.chartName,
760+
ChartName: chartLoc,
745761
CreateNamespace: true,
746762
Namespace: req.namespace,
747763
Wait: true,

internal/cmd/local/local/cmd_test.go

+14-4
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,14 @@ import (
2626
)
2727

2828
const portTest = 9999
29+
const testAirbyteChartLoc = "https://airbytehq.github.io/helm-charts/airbyte-1.2.3.tgz"
30+
31+
func testChartLocator(chartName, chartVersion string) string {
32+
if chartName == airbyteChartName && chartVersion == "" {
33+
return testAirbyteChartLoc
34+
}
35+
return chartName
36+
}
2937

3038
func TestCommand_Install(t *testing.T) {
3139
expChartRepoCnt := 0
@@ -48,7 +56,7 @@ func TestCommand_Install(t *testing.T) {
4856
{
4957
chart: helmclient.ChartSpec{
5058
ReleaseName: airbyteChartRelease,
51-
ChartName: airbyteChartName,
59+
ChartName: testAirbyteChartLoc,
5260
Namespace: airbyteNamespace,
5361
CreateNamespace: true,
5462
Wait: true,
@@ -106,7 +114,7 @@ func TestCommand_Install(t *testing.T) {
106114

107115
getChart: func(name string, _ *action.ChartPathOptions) (*chart.Chart, string, error) {
108116
switch {
109-
case name == airbyteChartName:
117+
case name == testAirbyteChartLoc:
110118
return &chart.Chart{Metadata: &chart.Metadata{Version: "test.airbyte.version"}}, "", nil
111119
case name == nginxChartName:
112120
return &chart.Chart{Metadata: &chart.Metadata{Version: "test.nginx.version"}}, "", nil
@@ -183,6 +191,7 @@ func TestCommand_Install(t *testing.T) {
183191
WithBrowserLauncher(func(url string) error {
184192
return nil
185193
}),
194+
WithChartLocator(testChartLocator),
186195
)
187196

188197
if err != nil {
@@ -215,7 +224,7 @@ func TestCommand_Install_ValuesFile(t *testing.T) {
215224
{
216225
chart: helmclient.ChartSpec{
217226
ReleaseName: airbyteChartRelease,
218-
ChartName: airbyteChartName,
227+
ChartName: testAirbyteChartLoc,
219228
Namespace: airbyteNamespace,
220229
CreateNamespace: true,
221230
Wait: true,
@@ -274,7 +283,7 @@ func TestCommand_Install_ValuesFile(t *testing.T) {
274283

275284
getChart: func(name string, _ *action.ChartPathOptions) (*chart.Chart, string, error) {
276285
switch {
277-
case name == airbyteChartName:
286+
case name == testAirbyteChartLoc:
278287
return &chart.Chart{Metadata: &chart.Metadata{Version: "test.airbyte.version"}}, "", nil
279288
case name == nginxChartName:
280289
return &chart.Chart{Metadata: &chart.Metadata{Version: "test.nginx.version"}}, "", nil
@@ -351,6 +360,7 @@ func TestCommand_Install_ValuesFile(t *testing.T) {
351360
WithBrowserLauncher(func(url string) error {
352361
return nil
353362
}),
363+
WithChartLocator(testChartLocator),
354364
)
355365

356366
if err != nil {

internal/cmd/local/local/locate.go

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package local
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/pterm/pterm"
7+
"helm.sh/helm/v3/pkg/cli"
8+
"helm.sh/helm/v3/pkg/getter"
9+
"helm.sh/helm/v3/pkg/repo"
10+
)
11+
12+
func locateLatestAirbyteChart(chartName, chartVersion string) string {
13+
pterm.Debug.Printf("getting helm chart %q with version %q\n", chartName, chartVersion)
14+
15+
// Helm will consider a local directory path named "airbyte/airbyte" to be a chart repo,
16+
// but it might not be, which causes errors like "Chart.yaml file is missing".
17+
// This trips up plenty of people, see: https://github.com/helm/helm/issues/7862
18+
//
19+
// Here we avoid that problem by figuring out the full URL of the airbyte chart,
20+
// which forces Helm to resolve the chart over HTTP and ignore local directories.
21+
// If the locator fails, fall back to the original helm behavior.
22+
if chartName == airbyteChartName && chartVersion == "" {
23+
if url, err := getLatestAirbyteChartUrlFromRepoIndex(airbyteRepoName, airbyteRepoURL); err == nil {
24+
pterm.Debug.Printf("determined latest airbyte chart url: %s\n", url)
25+
return url
26+
} else {
27+
pterm.Debug.Printf("error determining latest airbyte chart, falling back to default behavior: %s\n", err)
28+
}
29+
}
30+
31+
return chartName
32+
}
33+
34+
func getLatestAirbyteChartUrlFromRepoIndex(repoName, repoUrl string) (string, error) {
35+
chartRepo, err := repo.NewChartRepository(&repo.Entry{
36+
Name: repoName,
37+
URL: repoUrl,
38+
}, getter.All(cli.New()))
39+
if err != nil {
40+
return "", fmt.Errorf("unable to access repo index: %w", err)
41+
}
42+
43+
idxPath, err := chartRepo.DownloadIndexFile()
44+
if err != nil {
45+
return "", fmt.Errorf("unable to download index file: %w", err)
46+
}
47+
48+
idx, err := repo.LoadIndexFile(idxPath)
49+
if err != nil {
50+
return "", fmt.Errorf("unable to load index file (%s): %w", idxPath, err)
51+
}
52+
53+
airbyteEntry, ok := idx.Entries["airbyte"]
54+
if !ok {
55+
return "", fmt.Errorf("no entry for airbyte in repo index")
56+
}
57+
58+
if len(airbyteEntry) == 0 {
59+
return "", fmt.Errorf("no chart version found")
60+
}
61+
62+
latest := airbyteEntry[0]
63+
if len(latest.URLs) != 1 {
64+
return "", fmt.Errorf("unexpected number of URLs")
65+
}
66+
return airbyteRepoURL + "/" + latest.URLs[0], nil
67+
}

0 commit comments

Comments
 (0)