forked from fluxcd/source-controller
/
builder_remote.go
237 lines (213 loc) · 8.04 KB
/
builder_remote.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
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
/*
Copyright 2021 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package chart
import (
"context"
"fmt"
"io"
"os"
"path/filepath"
"github.com/Masterminds/semver/v3"
helmchart "helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chart/loader"
"helm.sh/helm/v3/pkg/chartutil"
"sigs.k8s.io/yaml"
"github.com/fluxcd/pkg/runtime/transform"
"github.com/aryan9600/source-controller/internal/fs"
"github.com/aryan9600/source-controller/internal/helm/repository"
)
type remoteChartBuilder struct {
remote *repository.ChartRepository
}
// NewRemoteBuilder returns a Builder capable of building a Helm
// chart with a RemoteReference in the given repository.ChartRepository.
func NewRemoteBuilder(repository *repository.ChartRepository) Builder {
return &remoteChartBuilder{
remote: repository,
}
}
// Build attempts to build a Helm chart with the given RemoteReference and
// BuildOptions, writing it to p.
// It returns a Build describing the produced (or from cache observed) chart
// written to p, or a BuildError.
//
// The latest version for the RemoteReference.Version is determined in the
// repository.ChartRepository, only downloading it if the version (including
// BuildOptions.VersionMetadata) differs from the current BuildOptions.CachedChart.
// BuildOptions.ValuesFiles changes are in this case not taken into account,
// and BuildOptions.Force should be used to enforce a rebuild.
//
// After downloading the chart, it is only packaged if required due to BuildOptions
// modifying the chart, otherwise the exact data as retrieved from the repository
// is written to p, after validating it to be a chart.
func (b *remoteChartBuilder) Build(_ context.Context, ref Reference, p string, opts BuildOptions) (*Build, error) {
remoteRef, ok := ref.(RemoteReference)
if !ok {
err := fmt.Errorf("expected remote chart reference")
return nil, &BuildError{Reason: ErrChartReference, Err: err}
}
if err := ref.Validate(); err != nil {
return nil, &BuildError{Reason: ErrChartReference, Err: err}
}
if err := b.remote.LoadFromCache(); err != nil {
err = fmt.Errorf("could not load repository index for remote chart reference: %w", err)
return nil, &BuildError{Reason: ErrChartPull, Err: err}
}
defer b.remote.Unload()
// Get the current version for the RemoteReference
cv, err := b.remote.Get(remoteRef.Name, remoteRef.Version)
if err != nil {
err = fmt.Errorf("failed to get chart version for remote reference: %w", err)
return nil, &BuildError{Reason: ErrChartPull, Err: err}
}
result := &Build{}
result.Name = cv.Name
result.Version = cv.Version
// Set build specific metadata if instructed
if opts.VersionMetadata != "" {
ver, err := semver.NewVersion(result.Version)
if err != nil {
err = fmt.Errorf("failed to parse version from chart metadata as SemVer: %w", err)
return nil, &BuildError{Reason: ErrChartMetadataPatch, Err: err}
}
if *ver, err = ver.SetMetadata(opts.VersionMetadata); err != nil {
err = fmt.Errorf("failed to set SemVer metadata on chart version: %w", err)
return nil, &BuildError{Reason: ErrChartMetadataPatch, Err: err}
}
result.Version = ver.String()
}
// If all the following is true, we do not need to download and/or build the chart:
// - Chart name from cached chart matches resolved name
// - Chart version from cached chart matches calculated version
// - BuildOptions.Force is False
if opts.CachedChart != "" && !opts.Force {
if curMeta, err := LoadChartMetadataFromArchive(opts.CachedChart); err == nil {
// If the cached metadata is corrupt, we ignore its existence
// and continue the build
if err = curMeta.Validate(); err == nil {
if result.Name == curMeta.Name && result.Version == curMeta.Version {
result.Path = opts.CachedChart
result.ValuesFiles = opts.GetValuesFiles()
return result, nil
}
}
}
}
// Download the package for the resolved version
res, err := b.remote.DownloadChart(cv)
if err != nil {
err = fmt.Errorf("failed to download chart for remote reference: %w", err)
return nil, &BuildError{Reason: ErrChartPull, Err: err}
}
// Use literal chart copy from remote if no custom values files options are
// set or build option version metadata isn't set.
if len(opts.GetValuesFiles()) == 0 && opts.VersionMetadata == "" {
if err = validatePackageAndWriteToPath(res, p); err != nil {
return nil, &BuildError{Reason: ErrChartPull, Err: err}
}
result.Path = p
return result, nil
}
// Load the chart and merge chart values
var chart *helmchart.Chart
if chart, err = loader.LoadArchive(res); err != nil {
err = fmt.Errorf("failed to load downloaded chart: %w", err)
return nil, &BuildError{Reason: ErrChartPackage, Err: err}
}
chart.Metadata.Version = result.Version
mergedValues, err := mergeChartValues(chart, opts.ValuesFiles)
if err != nil {
err = fmt.Errorf("failed to merge chart values: %w", err)
return nil, &BuildError{Reason: ErrValuesFilesMerge, Err: err}
}
// Overwrite default values with merged values, if any
if ok, err = OverwriteChartDefaultValues(chart, mergedValues); ok || err != nil {
if err != nil {
return nil, &BuildError{Reason: ErrValuesFilesMerge, Err: err}
}
result.ValuesFiles = opts.GetValuesFiles()
}
// Package the chart with the custom values
if err = packageToPath(chart, p); err != nil {
return nil, &BuildError{Reason: ErrChartPackage, Err: err}
}
result.Path = p
result.Packaged = true
return result, nil
}
// mergeChartValues merges the given chart.Chart Files paths into a single "values.yaml" map.
// It returns the merge result, or an error.
func mergeChartValues(chart *helmchart.Chart, paths []string) (map[string]interface{}, error) {
mergedValues := make(map[string]interface{})
for _, p := range paths {
cfn := filepath.Clean(p)
if cfn == chartutil.ValuesfileName {
mergedValues = transform.MergeMaps(mergedValues, chart.Values)
continue
}
var b []byte
for _, f := range chart.Files {
if f.Name == cfn {
b = f.Data
break
}
}
if b == nil {
return nil, fmt.Errorf("no values file found at path '%s'", p)
}
values := make(map[string]interface{})
if err := yaml.Unmarshal(b, &values); err != nil {
return nil, fmt.Errorf("unmarshaling values from '%s' failed: %w", p, err)
}
mergedValues = transform.MergeMaps(mergedValues, values)
}
return mergedValues, nil
}
// validatePackageAndWriteToPath atomically writes the packaged chart from reader
// to out while validating it by loading the chart metadata from the archive.
func validatePackageAndWriteToPath(reader io.Reader, out string) error {
tmpFile, err := os.CreateTemp("", filepath.Base(out))
if err != nil {
return fmt.Errorf("failed to create temporary file for chart: %w", err)
}
defer os.Remove(tmpFile.Name())
if _, err = tmpFile.ReadFrom(reader); err != nil {
_ = tmpFile.Close()
return fmt.Errorf("failed to write chart to file: %w", err)
}
if err = tmpFile.Close(); err != nil {
return err
}
meta, err := LoadChartMetadataFromArchive(tmpFile.Name())
if err != nil {
return fmt.Errorf("failed to load chart metadata from written chart: %w", err)
}
if err = meta.Validate(); err != nil {
return fmt.Errorf("failed to validate metadata of written chart: %w", err)
}
if err = fs.RenameWithFallback(tmpFile.Name(), out); err != nil {
return fmt.Errorf("failed to write chart to file: %w", err)
}
return nil
}
// pathIsDir returns a boolean indicating if the given path points to a directory.
// In case os.Stat on the given path returns an error it returns false as well.
func pathIsDir(p string) bool {
if p == "" {
return false
}
if i, err := os.Stat(p); err != nil || !i.IsDir() {
return false
}
return true
}