@@ -17,9 +17,15 @@ limitations under the License.
1717package envtest
1818
1919import (
20+ "archive/tar"
21+ "bytes"
22+ "compress/gzip"
2023 "context"
2124 "fmt"
25+ "io"
2226 "os"
27+ "path"
28+ "path/filepath"
2329 "strings"
2430 "time"
2531
@@ -31,8 +37,8 @@ import (
3137 "k8s.io/client-go/kubernetes/scheme"
3238 "k8s.io/client-go/rest"
3339 "sigs.k8s.io/controller-runtime/pkg/client"
34-
3540 "sigs.k8s.io/controller-runtime/pkg/client/config"
41+ "sigs.k8s.io/controller-runtime/pkg/envtest/internal"
3642 logf "sigs.k8s.io/controller-runtime/pkg/internal/log"
3743 "sigs.k8s.io/controller-runtime/pkg/internal/testing/controlplane"
3844 "sigs.k8s.io/controller-runtime/pkg/internal/testing/process"
@@ -147,6 +153,12 @@ type Environment struct {
147153 // values are merged.
148154 CRDDirectoryPaths []string
149155
156+ // DownloadBinaryAssets indicates that the binaries should be downloaded.
157+ DownloadBinaryAssets bool
158+
159+ // DownloadBinaryAssetsVersion is the version of binaries to download.
160+ DownloadBinaryAssetsVersion string
161+
150162 // BinaryAssetsDirectory is the path where the binaries required for the envtest are
151163 // located in the local environment. This field can be overridden by setting KUBEBUILDER_ASSETS.
152164 BinaryAssetsDirectory string
@@ -233,9 +245,20 @@ func (te *Environment) Start() (*rest.Config, error) {
233245 }
234246 }
235247
236- apiServer .Path = process .BinPathFinder ("kube-apiserver" , te .BinaryAssetsDirectory )
237- te .ControlPlane .Etcd .Path = process .BinPathFinder ("etcd" , te .BinaryAssetsDirectory )
238- te .ControlPlane .KubectlPath = process .BinPathFinder ("kubectl" , te .BinaryAssetsDirectory )
248+ if te .DownloadBinaryAssets { // FIXME: remove tmp files afterwards?
249+ apiServerPath , etcdPath , kubectlPath , err := downloadBinaryAssets (context .TODO (), te .BinaryAssetsDirectory , te .DownloadBinaryAssetsVersion )
250+ if err != nil {
251+ return nil , err
252+ }
253+
254+ apiServer .Path = apiServerPath
255+ te .ControlPlane .Etcd .Path = etcdPath
256+ te .ControlPlane .KubectlPath = kubectlPath
257+ } else {
258+ apiServer .Path = process .BinPathFinder ("kube-apiserver" , te .BinaryAssetsDirectory )
259+ te .ControlPlane .Etcd .Path = process .BinPathFinder ("etcd" , te .BinaryAssetsDirectory )
260+ te .ControlPlane .KubectlPath = process .BinPathFinder ("kubectl" , te .BinaryAssetsDirectory )
261+ }
239262
240263 if err := te .defaultTimeouts (); err != nil {
241264 return nil , fmt .Errorf ("failed to default controlplane timeouts: %w" , err )
@@ -303,6 +326,82 @@ func (te *Environment) Start() (*rest.Config, error) {
303326 return te .Config , nil
304327}
305328
329+ func downloadBinaryAssets (ctx context.Context , binaryAssetsDirectory , binaryAssetsVersion string ) (string , string , string , error ) {
330+ var downloadDir string
331+ if binaryAssetsDirectory != "" {
332+ downloadDir = binaryAssetsDirectory
333+ if ! fileExists (downloadDir ) {
334+ if err := os .Mkdir (binaryAssetsDirectory , 0700 ); err != nil {
335+ return "" , "" , "" , fmt .Errorf ("failed to create dir for envtest binaries %q: %w" , binaryAssetsDirectory , err )
336+ }
337+ }
338+ } else {
339+ var err error
340+ if downloadDir , err = os .MkdirTemp ("" , "envtest-binaries-" ); err != nil {
341+ return "" , "" , "" , fmt .Errorf ("failed to create tmp dir for envtest binaries: %w" , err )
342+ }
343+ }
344+
345+ apiServerPath := path .Join (downloadDir , "kube-apiserver" )
346+ etcdPath := path .Join (downloadDir , "etcd" )
347+ kubectlPath := path .Join (downloadDir , "kubectl" )
348+
349+ if fileExists (apiServerPath ) && fileExists (etcdPath ) && fileExists (kubectlPath ) {
350+ return apiServerPath , etcdPath , kubectlPath , nil
351+ }
352+
353+ buf := & bytes.Buffer {}
354+ if err := internal .DownloadBinaryAssets (ctx , binaryAssetsVersion , buf ); err != nil {
355+ return "" , "" , "" , fmt .Errorf ("failed to create tmp file to download envtest binaries: %w" , err )
356+ }
357+
358+ gzStream , err := gzip .NewReader (buf )
359+ if err != nil {
360+ return "" , "" , "" , fmt .Errorf ("failed to read TODO: %s" , err )
361+ }
362+ tarReader := tar .NewReader (gzStream )
363+
364+ var header * tar.Header
365+ for header , err = tarReader .Next (); err == nil ; header , err = tarReader .Next () {
366+ if header .Typeflag != tar .TypeReg { // Skipping non-regular file entry in archive
367+ continue
368+ }
369+
370+ // just dump all files to the main path, ignoring the prefixed directory
371+ // paths -- they're redundant. We also ignore bits for the most part (except for X),
372+ // preferfing our own scheme.
373+ fileName := filepath .Base (header .Name )
374+
375+ perms := 0555 & header .Mode // make sure we're at most r+x
376+
377+ binOut , err := os .OpenFile (path .Join (downloadDir , fileName ), os .O_RDWR | os .O_CREATE | os .O_EXCL | os .O_TRUNC , os .FileMode (perms ))
378+ if err != nil {
379+ if os .IsExist (err ) {
380+ continue
381+ }
382+ return "" , "" , "" , fmt .Errorf ("unable to create file %s from archive to disk for version-platform pair %s: %s" , fileName , downloadDir , err )
383+ }
384+ if err := func () error {
385+ defer binOut .Close ()
386+ if _ , err := io .Copy (binOut , tarReader ); err != nil {
387+ return fmt .Errorf ("unable to write file %s from archive to disk for version-platform pair %s" , fileName , downloadDir )
388+ }
389+ return nil
390+ }(); err != nil {
391+ return "" , "" , "" , err
392+ }
393+ }
394+
395+ return apiServerPath , etcdPath , kubectlPath , nil
396+ }
397+
398+ func fileExists (path string ) bool {
399+ if _ , err := os .Stat (path ); err == nil {
400+ return true
401+ }
402+ return false
403+ }
404+
306405// AddUser provisions a new user for connecting to this Environment. The user will
307406// have the specified name & belong to the specified groups.
308407//
0 commit comments