-
Notifications
You must be signed in to change notification settings - Fork 117
/
helm.go
221 lines (211 loc) · 6.98 KB
/
helm.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
package helm
import (
"context"
"fmt"
"io"
"net/http"
"os"
"os/exec"
"sort"
"strings"
"github.com/Masterminds/semver/v3"
"gopkg.in/yaml.v3"
"oras.land/oras-go/pkg/registry"
"oras.land/oras-go/pkg/registry/remote"
"oras.land/oras-go/pkg/registry/remote/auth"
libExec "github.com/akuity/kargo/internal/exec"
)
// SelectChartVersion connects to the Helm chart repository specified by
// repoURL and retrieves all available versions of the chart found therein. The
// repository can be either a classic chart repository (using HTTP/S) or a
// repository within an OCI registry. Classic chart repositories can contain
// differently named charts. When repoURL points to such a repository, the name
// argument must specify the name of the chart within the repository. In the
// case of a repository within an OCI registry, the URL implicitly points to a
// specific chart and the name argument must be empty. If no semverConstraint is
// provided (empty string is passed), then the version that is semantically
// greatest will be returned. If a semverConstraint is specified, then the
// semantically greatest version satisfying that constraint will be returned. If
// no version satisfies the constraint, the empty string is returned. Provided
// credentials may be nil for public repositories, but must be non-nil for
// private repositories.
func SelectChartVersion(
ctx context.Context,
repoURL string,
chart string,
semverConstraint string,
creds *Credentials,
) (string, error) {
var versions []string
var err error
if strings.HasPrefix(repoURL, "http://") ||
strings.HasPrefix(repoURL, "https://") {
versions, err =
getChartVersionsFromClassicRepo(repoURL, chart, creds)
} else if strings.HasPrefix(repoURL, "oci://") {
versions, err =
getChartVersionsFromOCIRepo(ctx, repoURL, creds)
} else {
return "", fmt.Errorf("repository URL %q is invalid", repoURL)
}
if err != nil {
return "", fmt.Errorf(
"error retrieving versions of chart %q from repository %q: %w",
chart,
repoURL,
err,
)
}
latestVersion, err := getLatestVersion(versions, semverConstraint)
if err != nil {
return "", fmt.Errorf(
"error determining latest version of chart %q from repository %q: %w",
chart,
repoURL,
err,
)
}
return latestVersion, nil
}
// getChartVersionsFromClassicRepo connects to the classic (HTTP/S) chart
// repository specified by repoURL and retrieves all available versions of the
// specified chart. The provided repoURL MUST begin with protocol http:// or
// https://. Provided credentials may be nil for public repositories, but must
// be non-nil for private repositories.
func getChartVersionsFromClassicRepo(
repoURL string,
chart string,
creds *Credentials,
) ([]string, error) {
indexURL := fmt.Sprintf("%s/index.yaml", strings.TrimSuffix(repoURL, "/"))
req, err := http.NewRequest(http.MethodGet, indexURL, nil)
if err != nil {
return nil, fmt.Errorf("error preparing HTTP/S request to %q: %w", indexURL, err)
}
if creds != nil {
req.SetBasicAuth(creds.Username, creds.Password)
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("error querying repository index at %q: %w", indexURL, err)
}
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf(
"received unexpected HTTP %d when querying repository index at %q",
res.StatusCode,
indexURL,
)
}
defer res.Body.Close()
resBodyBytes, err := io.ReadAll(res.Body)
if err != nil {
return nil, fmt.Errorf("error reading repository index from %q: %w", indexURL, err)
}
index := struct {
Entries map[string][]struct {
Version string `json:"version,omitempty"`
} `json:"entries,omitempty"`
}{}
if err = yaml.Unmarshal(resBodyBytes, &index); err != nil {
return nil, fmt.Errorf("error unmarshaling repository index from %q: %w", indexURL, err)
}
entries, ok := index.Entries[chart]
if !ok {
return nil, fmt.Errorf(
"no versions of chart %q found in repository index from %q",
chart,
indexURL,
)
}
versions := make([]string, len(entries))
for i, entry := range entries {
versions[i] = entry.Version
}
return versions, nil
}
// getChartVersionsFromOCIRepo connects to the OCI repository specified by
// repoURL and retrieves all available versions of the specified chart. Provided
// credentials may be nil for public repositories, but must be non-nil for
// private repositories.
func getChartVersionsFromOCIRepo(
ctx context.Context,
repoURL string,
creds *Credentials,
) ([]string, error) {
ref, err := registry.ParseReference(strings.TrimPrefix(repoURL, "oci://"))
if err != nil {
return nil, fmt.Errorf("error parsing repository URL %q: %w", repoURL, err)
}
rep := &remote.Repository{
Reference: ref,
Client: &auth.Client{
Credential: func(context.Context, string) (auth.Credential, error) {
if creds != nil {
return auth.Credential{
Username: creds.Username,
Password: creds.Password,
}, nil
}
return auth.Credential{}, nil
},
},
}
versions := make([]string, 0, rep.TagListPageSize)
if err := rep.Tags(ctx, func(t []string) error {
versions = append(versions, t...)
return nil
}); err != nil {
return nil, fmt.Errorf("error retrieving versions of chart from repository %q: %w", repoURL, err)
}
return versions, nil
}
// getLatestVersion returns the semantically greatest version from the versions
// provided which satisfies the provided constraints. If no constraints are
// specified (the empty string is passed), the absolute semantically greatest
// version will be returned. The empty string will be returned when the provided
// list of versions is nil or empty.
func getLatestVersion(versions []string, constraintStr string) (string, error) {
semvers := make([]*semver.Version, 0, len(versions))
for _, version := range versions {
if semverVersion, err := semver.NewVersion(version); err == nil {
semvers = append(semvers, semverVersion)
}
}
if len(semvers) == 0 {
return "", nil
}
sort.Sort(semver.Collection(semvers))
if constraintStr == "" {
return semvers[len(semvers)-1].String(), nil
}
constraint, err := semver.NewConstraint(constraintStr)
if err != nil {
return "", fmt.Errorf("error parsing constraint %q: %w", constraintStr, err)
}
for i := len(semvers) - 1; i >= 0; i-- {
if constraint.Check(semvers[i]) {
return semvers[i].String(), nil
}
}
return "", nil
}
func UpdateChartDependencies(homePath, chartPath string) error {
cmd := exec.Command("helm", "dependency", "update", chartPath)
cmd.Env = append(cmd.Env, os.Environ()...)
cmd.Env = append(cmd.Env, fmt.Sprintf("HOME=%s", homePath))
if _, err := libExec.Exec(cmd); err != nil {
return fmt.Errorf("error running `helm dependency update` for chart at %q: %w", chartPath, err)
}
return nil
}
// NormalizeChartRepositoryURL normalizes a chart repository URL for purposes
// of comparison. Crucially, this function removes the oci:// prefix from the
// URL if there is one.
func NormalizeChartRepositoryURL(repo string) string {
return strings.TrimPrefix(
strings.ToLower(
strings.TrimSpace(repo),
),
"oci://",
)
}