44 "archive/tar"
55 "compress/gzip"
66 "context"
7+ "crypto/sha512"
8+ "encoding/hex"
79 "errors"
810 "fmt"
911 "io"
@@ -17,9 +19,34 @@ import (
1719// DefaultVersion is the ClickHouse release the goldens are generated with.
1820// Bumping it is a deliberate change: the query tree format and type
1921// inference can shift between releases, so regenerate and review the goldens
20- // after changing it.
22+ // after changing it, and add the new release's assets to the table below .
2123const DefaultVersion = "25.8.2.29"
2224
25+ // asset is one downloadable build of ClickHouse. Linux builds are published
26+ // as clickhouse-common-static tarballs holding the binary at
27+ // usr/bin/clickhouse; macOS builds are published as bare binaries.
28+ type asset struct {
29+ Version string
30+ OS string
31+ Arch string
32+ Name string
33+ SHA512 string
34+ }
35+
36+ // assets lists every build Install knows how to fetch, with the SHA-512 of
37+ // the download. A version that is not in this table cannot be installed:
38+ // verifying the download is the point of the table.
39+ //
40+ // The tarball checksums are the ones in the .sha512 files ClickHouse
41+ // publishes next to them. ClickHouse publishes no checksum for the macOS
42+ // binaries, so those were computed from the downloads.
43+ var assets = []asset {
44+ {"25.8.2.29" , "linux" , "amd64" , "clickhouse-common-static-25.8.2.29-amd64.tgz" , "6ff0aa1ffac6e564970174422ecde0d645cdb96812247a6e544d39cad6d78a514265f90a2bc7b4bad49903cea96eddd16a415a45b2aeaf9164461be76331bdee" },
45+ {"25.8.2.29" , "linux" , "arm64" , "clickhouse-common-static-25.8.2.29-arm64.tgz" , "68204ca4d4e472790f808ee376251fae82e58066a31f35a40d15d442ce5988d697f18a1208d28b8bb8e2dfad4b20b7fcb5107e2178472abcd97251b8de7f058e" },
46+ {"25.8.2.29" , "darwin" , "amd64" , "clickhouse-macos" , "2805805ad2506e37a3e71b4ae9e797bdc010a9368dc28e99bcaaa2c70a72cfdd031c0fce8fc304248fad73211d28d645f75c6432cfae1e1e54d72d04e8626cd4" },
47+ {"25.8.2.29" , "darwin" , "arm64" , "clickhouse-macos-aarch64" , "4c9237e85c8d4e1aced2b339b32e086b4f23fa14f99754b056a7e33dda88c4fb5d52e09e29d20181ec5b580caadb00f36613802e71755ff34927535eb8babf79" },
48+ }
49+
2350// releaseTag returns the GitHub release tag for a version. ClickHouse tags
2451// its March and August releases as LTS and everything else as stable.
2552func releaseTag (version string ) (string , error ) {
@@ -38,27 +65,33 @@ func releaseTag(version string) (string, error) {
3865 return "v" + version + suffix , nil
3966}
4067
41- // releaseAsset returns the download URL for a platform and whether it is a
42- // tarball holding the binary at usr/bin/clickhouse rather than the bare
43- // binary. Linux builds are only published as tarballs; macOS builds only as
44- // bare binaries.
45- func releaseAsset (version , goos , goarch string ) (url string , tarball bool , err error ) {
46- tag , err := releaseTag (version )
68+ // releaseAsset finds the build for a platform in the table.
69+ func releaseAsset (version , goos , goarch string ) (asset , error ) {
70+ for _ , a := range assets {
71+ if a .Version == version && a .OS == goos && a .Arch == goarch {
72+ return a , nil
73+ }
74+ }
75+ for _ , a := range assets {
76+ if a .Version == version {
77+ return asset {}, fmt .Errorf ("no ClickHouse %s build is listed for %s/%s" , version , goos , goarch )
78+ }
79+ }
80+ return asset {}, fmt .Errorf ("ClickHouse %s is not in the asset table; add its downloads and checksums to install.go" , version )
81+ }
82+
83+ // url is the asset's download address on GitHub.
84+ func (a asset ) url () (string , error ) {
85+ tag , err := releaseTag (a .Version )
4786 if err != nil {
48- return "" , false , err
49- }
50- base := "https://github.com/ClickHouse/ClickHouse/releases/download/" + tag + "/"
51- switch goos + "/" + goarch {
52- case "linux/amd64" :
53- return base + "clickhouse-common-static-" + version + "-amd64.tgz" , true , nil
54- case "linux/arm64" :
55- return base + "clickhouse-common-static-" + version + "-arm64.tgz" , true , nil
56- case "darwin/amd64" :
57- return base + "clickhouse-macos" , false , nil
58- case "darwin/arm64" :
59- return base + "clickhouse-macos-aarch64" , false , nil
60- }
61- return "" , false , fmt .Errorf ("no ClickHouse build is published for %s/%s" , goos , goarch )
87+ return "" , err
88+ }
89+ return "https://github.com/ClickHouse/ClickHouse/releases/download/" + tag + "/" + a .Name , nil
90+ }
91+
92+ // tarball reports whether the download is an archive rather than the binary.
93+ func (a asset ) tarball () bool {
94+ return strings .HasSuffix (a .Name , ".tgz" )
6295}
6396
6497// cachedBinary is where Install puts the binary for a version.
@@ -87,7 +120,8 @@ func Locate() (string, error) {
87120}
88121
89122// Install downloads the clickhouse binary for a version into the cache and
90- // returns its path. It is a no-op when the version is already cached.
123+ // returns its path. It is a no-op when the version is already cached. The
124+ // download is checked against the table's SHA-512 before it is installed.
91125func Install (ctx context.Context , version , goos , goarch string , progress io.Writer ) (string , error ) {
92126 dest , err := cachedBinary (version )
93127 if err != nil {
@@ -96,7 +130,11 @@ func Install(ctx context.Context, version, goos, goarch string, progress io.Writ
96130 if _ , err := os .Stat (dest ); err == nil {
97131 return dest , nil
98132 }
99- url , tarball , err := releaseAsset (version , goos , goarch )
133+ a , err := releaseAsset (version , goos , goarch )
134+ if err != nil {
135+ return "" , err
136+ }
137+ url , err := a .url ()
100138 if err != nil {
101139 return "" , err
102140 }
@@ -118,17 +156,21 @@ func Install(ctx context.Context, version, goos, goarch string, progress io.Writ
118156 return "" , fmt .Errorf ("downloading %s: %s" , url , resp .Status )
119157 }
120158
121- // Write next to the destination and rename so a partial download never
122- // masquerades as an installed binary.
159+ // Write next to the destination and rename so a partial or corrupt
160+ // download never masquerades as an installed binary.
123161 tmp , err := os .CreateTemp (filepath .Dir (dest ), "clickhouse-*.partial" )
124162 if err != nil {
125163 return "" , err
126164 }
127165 defer os .Remove (tmp .Name ())
128166
129- var src io.Reader = resp .Body
130- if tarball {
131- src , err = binaryInTarball (resp .Body )
167+ // Hash every byte that comes off the wire, including the parts of a
168+ // tarball after the binary, which extraction would otherwise not read.
169+ sum := sha512 .New ()
170+ body := io .TeeReader (resp .Body , sum )
171+ var src io.Reader = body
172+ if a .tarball () {
173+ src , err = binaryInTarball (body )
132174 if err != nil {
133175 return "" , fmt .Errorf ("downloading %s: %w" , url , err )
134176 }
@@ -137,9 +179,16 @@ func Install(ctx context.Context, version, goos, goarch string, progress io.Writ
137179 tmp .Close ()
138180 return "" , err
139181 }
182+ if _ , err := io .Copy (io .Discard , body ); err != nil {
183+ tmp .Close ()
184+ return "" , err
185+ }
140186 if err := tmp .Close (); err != nil {
141187 return "" , err
142188 }
189+ if got := hex .EncodeToString (sum .Sum (nil )); got != a .SHA512 {
190+ return "" , fmt .Errorf ("downloading %s: SHA-512 mismatch: got %s, want %s" , url , got , a .SHA512 )
191+ }
143192 if err := os .Chmod (tmp .Name (), 0o755 ); err != nil {
144193 return "" , err
145194 }
0 commit comments