/
generate.go
291 lines (251 loc) · 7.72 KB
/
generate.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
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
// hash-generator updates the binary hashes and kubeadm patches in versions.go in place.
//
// This binary is usually invoked by the //bazel/ci:go_generate target, but you can run it
// manually, too. Clear a hash or a data URL in versions.go and execute
//
// bazel run //internal/versions/hash-generator -- --update=false $PWD/internal/versions/versions.go
package main
import (
"bytes"
"context"
"crypto/sha256"
"encoding/json"
"flag"
"fmt"
"go/ast"
"go/parser"
"go/printer"
"go/token"
"io"
"log"
"net/http"
"os"
"slices"
"strings"
"github.com/regclient/regclient"
"github.com/regclient/regclient/types/ref"
"github.com/vincent-petithory/dataurl"
"golang.org/x/tools/go/ast/astutil"
"k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
"k8s.io/kubernetes/cmd/kubeadm/app/images"
)
const (
defaultRegistry = "registry.k8s.io"
etcdComponent = "etcd"
defaultFilePath = "./versions.go"
)
var supportedComponents = []string{"kube-apiserver", "kube-controller-manager", "kube-scheduler", "etcd"}
func quote(s string) string {
return fmt.Sprintf(`"%s"`, s)
}
func unquote(s string) string {
return strings.TrimPrefix(strings.TrimSuffix(s, `"`), `"`)
}
// pinKubernetesImage takes a component and a version and returns the corresponding container image pinned by hash.
//
// The version string is a Kubernetes version tag, which is used to derive the tags of the component images.
// The image hash is obtained directly from the default registry, registry.k8s.io.
func pinKubernetesImage(comp, ver string) (string, error) {
if !slices.Contains(supportedComponents, comp) {
return "", fmt.Errorf("k8s component %q not supported: valid components: %#v", comp, supportedComponents)
}
ref := ref.Ref{Scheme: "reg", Registry: defaultRegistry, Repository: comp, Tag: ver}
if comp == etcdComponent {
cfg := &kubeadm.ClusterConfiguration{
KubernetesVersion: ver,
ImageRepository: defaultRegistry,
}
img := images.GetEtcdImage(cfg)
_, tag, _ := strings.Cut(img, ":")
ref.Tag = tag
}
log.Printf("Getting hash for image %#v", ref)
rc := regclient.New()
m, err := rc.ManifestGet(context.Background(), ref)
if err != nil {
return "", fmt.Errorf("could not obtain image manifest: %w", err)
}
return fmt.Sprintf("%s/%s:%s@%s", ref.Registry, ref.Repository, ref.Tag, m.GetDescriptor().Digest.String()), nil
}
func generateKubeadmPatch(comp, ver string) (string, error) {
img, err := pinKubernetesImage(comp, ver)
if err != nil {
return "", err
}
content, err := json.Marshal([]map[string]string{{
"op": "replace",
"path": "/spec/containers/0/image",
"value": img,
}})
if err != nil {
return "", err
}
return dataurl.New(content, "application/json").String(), nil
}
// hashURLContent downloads a binary blob from the given URL and calculates its SHA256 hash.
//
// URLs passed to this function are expected to have upstream signatures with a .sha256 suffix. This upstream signature
// will be verified, too.
//
// nolint:noctx // This is a cli that does not benefit from passing contexts around.
func hashURLContent(url string) (string, error) {
resp, err := http.Get(url)
if err != nil {
return "", fmt.Errorf("could not fetch URL: %w", err)
}
defer resp.Body.Close()
// Check server response
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("unexpected HTTP response code: %d", resp.StatusCode)
}
// Generate SHA256 hash of the file
sha := sha256.New()
if _, err := io.Copy(sha, resp.Body); err != nil {
return "", fmt.Errorf("could not calculate response body hash: %w", err)
}
fileHash := sha.Sum(nil)
resp, err = http.Get(url + ".sha256")
if err != nil {
return "", fmt.Errorf("could not fetch upstream digest: %w", err)
}
defer resp.Body.Close()
// Check server response
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("unexpected HTTP response code for upstream digest: %d", resp.StatusCode)
}
// Compare hashes
// Take the first 64 ascii characters = 32 bytes.
// Some .sha256 files contain additional information afterwards.
upstreamHash := make([]byte, 64)
if _, err = resp.Body.Read(upstreamHash); err != nil {
return "", fmt.Errorf("could not read upstream hash: %w", err)
}
if string(upstreamHash) != fmt.Sprintf("%x", fileHash) {
return "", fmt.Errorf("computed hash %x does not match upstream hash %s", fileHash, string(upstreamHash))
}
return fmt.Sprintf("sha256:%x", fileHash), nil
}
type updater struct {
k8sVersion string
}
// maybeSetVersion keeps track of the ambient ClusterVersion of components.
func (u *updater) maybeSetVersion(n ast.Node) {
kv, ok := n.(*ast.KeyValueExpr)
if !ok {
return
}
key, ok := kv.Key.(*ast.Ident)
if !ok || key.Name != "ClusterVersion" {
return
}
val, ok := kv.Value.(*ast.BasicLit)
if !ok || val.Kind != token.STRING {
return
}
u.k8sVersion = val.Value[1 : len(val.Value)-1]
}
func (u *updater) updateComponents(cursor *astutil.Cursor) bool {
n := cursor.Node()
u.maybeSetVersion(n)
//
// Find CompositeLit of type 'components.Components'
//
comp, ok := n.(*ast.CompositeLit)
if !ok {
return true
}
selExpr, ok := comp.Type.(*ast.SelectorExpr)
if !ok {
return true
}
if selExpr.Sel.Name != "Components" {
return true
}
xIdent, ok := selExpr.X.(*ast.Ident)
if !ok {
return true
}
if xIdent.Name != "components" {
return true
}
log.Printf("Iterating over components for cluster version %q", u.k8sVersion)
//
// Iterate over the components
//
for _, componentElt := range comp.Elts {
component := componentElt.(*ast.CompositeLit)
var url, hash, installPath *ast.KeyValueExpr
for _, e := range component.Elts {
kv, ok := e.(*ast.KeyValueExpr)
if !ok {
continue
}
ident, ok := kv.Key.(*ast.Ident)
if !ok {
continue
}
switch ident.Name {
case "Url":
url = kv
case "Hash":
hash = kv
case "InstallPath":
installPath = kv
}
}
urlValue := unquote(url.Value.(*ast.BasicLit).Value)
if urlValue == "" || strings.HasPrefix(urlValue, "data:") {
// This can't be a downloadable component, so we assume this is supposed to be a kubeadm patch.
if urlValue != "" && !*updateHash {
continue
}
// all patch InstallPaths look like `patchFilePath("$COMPONENT")`
comp := unquote(installPath.Value.(*ast.CallExpr).Args[0].(*ast.BasicLit).Value)
log.Println("Generating kubeadm patch for", comp)
dataURL, err := generateKubeadmPatch(comp, u.k8sVersion)
if err != nil {
log.Fatalf("Could not generate kubeadm patch for %q: %v", comp, err)
}
url.Value.(*ast.BasicLit).Value = quote(dataURL)
} else {
if hash.Value.(*ast.BasicLit).Value != `""` && !*updateHash {
continue
}
log.Println("Generating hash for", urlValue)
h, err := hashURLContent(urlValue)
if err != nil {
log.Fatalf("Could not hash URL %q: %v", urlValue, err)
}
hash.Value.(*ast.BasicLit).Value = quote(h)
}
}
return true
}
var updateHash = flag.Bool("update", true, "update existing hashes and data URLs")
func main() {
log.Println("Generating hashes...")
flag.Parse()
filePath := flag.Arg(0)
if filePath == "" {
filePath = defaultFilePath
}
fset := token.NewFileSet()
file, err := parser.ParseFile(fset, filePath, nil, parser.ParseComments)
if err != nil {
log.Fatalf("Could not parse file %q: %v", filePath, err)
}
updater := &updater{}
newFile := astutil.Apply(file, updater.updateComponents, nil)
var buf bytes.Buffer
printConfig := printer.Config{Mode: printer.UseSpaces | printer.TabIndent, Tabwidth: 8}
if err = printConfig.Fprint(&buf, fset, newFile); err != nil {
log.Fatalf("Could not format file %q: %v", filePath, err)
}
if err := os.WriteFile(filePath, buf.Bytes(), 0o644); err != nil {
log.Fatalf("Could not write file %q: %v", filePath, err)
}
}