-
Notifications
You must be signed in to change notification settings - Fork 4.7k
/
builder.go
387 lines (321 loc) · 12.3 KB
/
builder.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
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
/*
Copyright 2017 The Kubernetes 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 assets
import (
"fmt"
"net/url"
"os"
"path"
"regexp"
"strings"
"time"
"github.com/blang/semver/v4"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/crane"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/klog/v2"
"k8s.io/kops/pkg/apis/kops"
"k8s.io/kops/pkg/apis/kops/util"
"k8s.io/kops/pkg/featureflag"
"k8s.io/kops/pkg/kubemanifest"
"k8s.io/kops/pkg/values"
"k8s.io/kops/util/pkg/hashing"
"k8s.io/kops/util/pkg/mirrors"
"k8s.io/kops/util/pkg/vfs"
)
// AssetBuilder discovers and remaps assets.
type AssetBuilder struct {
vfsContext *vfs.VFSContext
ImageAssets []*ImageAsset
FileAssets []*FileAsset
AssetsLocation *kops.AssetsSpec
GetAssets bool
// KubernetesVersion is the version of kubernetes we are installing
KubernetesVersion semver.Version
// StaticManifests records manifests used by nodeup:
// * e.g. sidecar manifests for static pods run by kubelet
StaticManifests []*StaticManifest
// StaticFiles records static files:
// * Configuration files supporting static pods
StaticFiles []*StaticFile
}
type StaticFile struct {
// Path is the path to the manifest.
Path string
// Content holds the desired file contents.
Content string
// The static manifest will only be applied to instances matching the specified role
Roles []kops.InstanceGroupRole
}
type StaticManifest struct {
// Key is the unique identifier of the manifest
Key string
// Path is the path to the manifest.
Path string
// The static manifest will only be applied to instances matching the specified role
Roles []kops.InstanceGroupRole
}
// ImageAsset models an image's location.
type ImageAsset struct {
// DownloadLocation will be the name of the image we should run.
// This is used to copy an image to a ContainerRegistry.
DownloadLocation string
// CanonicalLocation will be the source location of the image.
CanonicalLocation string
}
// FileAsset models a file's location.
type FileAsset struct {
// DownloadURL is the URL from which the cluster should download the asset.
DownloadURL *url.URL
// CanonicalURL is the canonical location of the asset, for example as distributed by the kops project
CanonicalURL *url.URL
// SHAValue is the SHA hash of the FileAsset.
SHAValue string
}
// NewAssetBuilder creates a new AssetBuilder.
func NewAssetBuilder(vfsContext *vfs.VFSContext, assets *kops.AssetsSpec, kubernetesVersion string, getAssets bool) *AssetBuilder {
a := &AssetBuilder{
vfsContext: vfsContext,
AssetsLocation: assets,
GetAssets: getAssets,
}
version, err := util.ParseKubernetesVersion(kubernetesVersion)
if err != nil {
// This should have already been validated
klog.Fatalf("unexpected error from ParseKubernetesVersion %s: %v", kubernetesVersion, err)
}
a.KubernetesVersion = *version
return a
}
// RemapManifest transforms a kubernetes manifest.
// Whenever we are building a Task that includes a manifest, we should pass it through RemapManifest first.
// This will:
// * rewrite the images if they are being redirected to a mirror, and ensure the image is uploaded
func (a *AssetBuilder) RemapManifest(data []byte) ([]byte, error) {
objects, err := kubemanifest.LoadObjectsFrom(data)
if err != nil {
return nil, err
}
for _, object := range objects {
if err := object.RemapImages(a.RemapImage); err != nil {
return nil, fmt.Errorf("error remapping images: %v", err)
}
}
return objects.ToYAML()
}
// RemapImage normalizes a containers location if a user sets the AssetsLocation ContainerRegistry location.
func (a *AssetBuilder) RemapImage(image string) (string, error) {
asset := &ImageAsset{
DownloadLocation: image,
CanonicalLocation: image,
}
if strings.HasPrefix(image, "registry.k8s.io/kops/dns-controller:") {
// To use user-defined DNS Controller:
// 1. DOCKER_REGISTRY=[your docker hub repo] make dns-controller-push
// 2. export DNSCONTROLLER_IMAGE=[your docker hub repo]
// 3. make kops and create/apply cluster
override := os.Getenv("DNSCONTROLLER_IMAGE")
if override != "" {
image = override
}
}
if strings.HasPrefix(image, "registry.k8s.io/kops/kops-controller:") {
// To use user-defined DNS Controller:
// 1. DOCKER_REGISTRY=[your docker hub repo] make kops-controller-push
// 2. export KOPSCONTROLLER_IMAGE=[your docker hub repo]
// 3. make kops and create/apply cluster
override := os.Getenv("KOPSCONTROLLER_IMAGE")
if override != "" {
image = override
}
}
if strings.HasPrefix(image, "registry.k8s.io/kops/kube-apiserver-healthcheck:") {
override := os.Getenv("KUBE_APISERVER_HEALTHCHECK_IMAGE")
if override != "" {
image = override
}
}
if a.AssetsLocation != nil && a.AssetsLocation.ContainerProxy != nil {
containerProxy := strings.TrimSuffix(*a.AssetsLocation.ContainerProxy, "/")
normalized := image
// If the image name contains only a single / we need to determine if the image is located on docker-hub or if it's using a convenient URL,
// like registry.k8s.io/<image-name> or registry.k8s.io/<image-name>
// In case of a hub image it should be sufficient to just prepend the proxy url, producing eg docker-proxy.example.com/weaveworks/weave-kube
if strings.Count(normalized, "/") <= 1 && !strings.ContainsAny(strings.Split(normalized, "/")[0], ".:") {
normalized = containerProxy + "/" + normalized
} else {
re := regexp.MustCompile(`^[^/]+`)
normalized = re.ReplaceAllString(normalized, containerProxy)
}
asset.DownloadLocation = normalized
// Run the new image
image = asset.DownloadLocation
}
if a.AssetsLocation != nil && a.AssetsLocation.ContainerRegistry != nil {
registryMirror := *a.AssetsLocation.ContainerRegistry
normalized := image
// Remove the 'standard' kubernetes image prefixes, just for sanity
normalized = strings.TrimPrefix(normalized, "registry.k8s.io/")
// When assembling the cluster spec, kops may call the option more then once until the config converges
// This means that this function may me called more than once on the same image
// It this is pass is the second one, the image will already have been normalized with the containerRegistry settings
// If this is the case, passing though the process again will re-prepend the container registry again
// and again, causing the spec to never converge and the config build to fail.
if !strings.HasPrefix(normalized, registryMirror+"/") {
// We can't nest arbitrarily
// Some risk of collisions, but also -- and __ in the names appear to be blocked by docker hub
normalized = strings.Replace(normalized, "/", "-", -1)
asset.DownloadLocation = registryMirror + "/" + normalized
}
// Run the new image
image = asset.DownloadLocation
}
a.ImageAssets = append(a.ImageAssets, asset)
if !featureflag.ImageDigest.Enabled() || os.Getenv("KOPS_BASE_URL") != "" {
return image, nil
}
if strings.Contains(image, "@") {
return image, nil
}
digest, err := crane.Digest(image, crane.WithAuthFromKeychain(authn.DefaultKeychain))
if err != nil {
klog.Warningf("failed to digest image %q: %s", image, err)
return image, nil
}
return image + "@" + digest, nil
}
// RemapFileAndSHA returns a remapped URL for the file, if AssetsLocation is defined.
// It also returns the SHA hash of the file.
func (a *AssetBuilder) RemapFileAndSHA(fileURL *url.URL) (*url.URL, *hashing.Hash, error) {
if fileURL == nil {
return nil, nil, fmt.Errorf("unable to remap a nil URL")
}
fileAsset := &FileAsset{
DownloadURL: fileURL,
CanonicalURL: fileURL,
}
if a.AssetsLocation != nil && a.AssetsLocation.FileRepository != nil {
normalizedFileURL, err := a.remapURL(fileURL)
if err != nil {
return nil, nil, err
}
fileAsset.DownloadURL = normalizedFileURL
klog.V(4).Infof("adding remapped file: %+v", fileAsset)
}
h, err := a.findHash(fileAsset)
if err != nil {
return nil, nil, err
}
fileAsset.SHAValue = h.Hex()
klog.V(8).Infof("adding file: %+v", fileAsset)
a.FileAssets = append(a.FileAssets, fileAsset)
return fileAsset.DownloadURL, h, nil
}
// RemapFileAndSHAValue returns a remapped URL for the file without a SHA file in object storage, if AssetsLocation is defined.
func (a *AssetBuilder) RemapFileAndSHAValue(fileURL *url.URL, shaValue string) (*url.URL, error) {
if fileURL == nil {
return nil, fmt.Errorf("unable to remap a nil URL")
}
fileAsset := &FileAsset{
DownloadURL: fileURL,
CanonicalURL: fileURL,
SHAValue: shaValue,
}
if a.AssetsLocation != nil && a.AssetsLocation.FileRepository != nil {
normalizedFile, err := a.remapURL(fileURL)
if err != nil {
return nil, err
}
fileAsset.DownloadURL = normalizedFile
klog.V(4).Infof("adding remapped file: %q", fileAsset.DownloadURL.String())
}
klog.V(8).Infof("adding file: %+v", fileAsset)
a.FileAssets = append(a.FileAssets, fileAsset)
return fileAsset.DownloadURL, nil
}
// FindHash returns the hash value of a FileAsset.
func (a *AssetBuilder) findHash(file *FileAsset) (*hashing.Hash, error) {
// If the phase is "assets" we use the CanonicalFileURL,
// but during other phases we use the hash from the FileRepository or the base kops path.
// We do not want to just test for CanonicalFileURL as it is defined in
// other phases, but is not used to test for the SHA.
// This prevents a chicken and egg problem where the file is not yet in the FileRepository.
//
// assets phase -> get the sha file from the source / CanonicalFileURL
// any other phase -> get the sha file from the kops base location or the FileRepository
//
// TLDR; we use the file.CanonicalFileURL during assets phase, and use file.FileUrl the
// rest of the time. If not we get a chicken and the egg problem where we are reading the sha file
// before it exists.
u := file.DownloadURL
if a.GetAssets {
u = file.CanonicalURL
}
if u == nil {
return nil, fmt.Errorf("file url is not defined")
}
// We now prefer sha256 hashes
for backoffSteps := 1; backoffSteps <= 3; backoffSteps++ {
// We try first with a short backoff, so we don't
// waste too much time looking for files that don't
// exist before trying the next one
backoff := wait.Backoff{
Duration: 500 * time.Millisecond,
Factor: 2,
Steps: backoffSteps,
}
for _, ext := range []string{".sha256", ".sha256sum"} {
for _, mirror := range mirrors.FindURLMirrors(u.String()) {
hashURL := mirror + ext
klog.V(3).Infof("Trying to read hash fie: %q", hashURL)
b, err := a.vfsContext.ReadFile(hashURL, vfs.WithBackoff(backoff))
if err != nil {
// Try to log without being too alarming - issue #7550
klog.V(2).Infof("Unable to read hash file %q: %v", hashURL, err)
continue
}
hashString := strings.TrimSpace(string(b))
klog.V(2).Infof("Found hash %q for %q", hashString, u)
// Accept a hash string that is `<hash> <filename>`
fields := strings.Fields(hashString)
if len(fields) == 0 {
klog.Infof("Hash file was empty %q", hashURL)
continue
}
return hashing.FromString(fields[0])
}
if ext == ".sha256" {
klog.V(2).Infof("Unable to read new sha256 hash file (is this an older/unsupported kubernetes release?)")
}
}
}
if a.AssetsLocation != nil && a.AssetsLocation.FileRepository != nil {
return nil, fmt.Errorf("you might have not staged your files correctly, please execute 'kops get assets --copy'")
}
return nil, fmt.Errorf("cannot determine hash for %q (have you specified a valid file location?)", u)
}
func (a *AssetBuilder) remapURL(canonicalURL *url.URL) (*url.URL, error) {
f := ""
if a.AssetsLocation != nil {
f = values.StringValue(a.AssetsLocation.FileRepository)
}
if f == "" {
return nil, fmt.Errorf("assetsLocation.fileRepository must be set to remap asset %v", canonicalURL)
}
fileRepo, err := url.Parse(f)
if err != nil {
return nil, fmt.Errorf("unable to parse assetsLocation.fileRepository %q: %v", f, err)
}
fileRepo.Path = path.Join(fileRepo.Path, canonicalURL.Path)
return fileRepo, nil
}