From 06aad71cf4e95da189ce5d3b1a9f9b8dde5de90e Mon Sep 17 00:00:00 2001 From: Shiming Zhang Date: Mon, 1 Apr 2024 09:58:52 +0800 Subject: [PATCH] Refactor download and progress bar --- go.mod | 13 +++ go.sum | 34 +++++++ pkg/kwokctl/cmd/create/cluster/cluster.go | 4 + pkg/kwokctl/runtime/compose/cluster.go | 3 + pkg/kwokctl/runtime/exec.go | 82 ++++++++++++++--- pkg/kwokctl/runtime/kind/cluster.go | 3 + pkg/utils/exec/pull.go | 82 ----------------- pkg/utils/file/download.go | 68 +++++++------- pkg/utils/file/progress_bar.go | 60 ------------- pkg/utils/image/doc.go | 18 ++++ pkg/utils/image/pull.go | 99 +++++++++++++++++++++ pkg/utils/progressbar/doc.go | 18 ++++ pkg/utils/progressbar/format.go | 103 ++++++++++++++++++++++ pkg/utils/progressbar/progressbar.go | 80 +++++++++++++++++ pkg/utils/progressbar/reader.go | 70 +++++++++++++++ pkg/utils/width/doc.go | 18 ++++ pkg/utils/width/width.go | 73 +++++++++++++++ pkg/utils/width/width_test.go | 70 +++++++++++++++ 18 files changed, 709 insertions(+), 189 deletions(-) delete mode 100644 pkg/utils/exec/pull.go delete mode 100644 pkg/utils/file/progress_bar.go create mode 100644 pkg/utils/image/doc.go create mode 100644 pkg/utils/image/pull.go create mode 100644 pkg/utils/progressbar/doc.go create mode 100644 pkg/utils/progressbar/format.go create mode 100644 pkg/utils/progressbar/progressbar.go create mode 100644 pkg/utils/progressbar/reader.go create mode 100644 pkg/utils/width/doc.go create mode 100644 pkg/utils/width/width.go create mode 100644 pkg/utils/width/width_test.go diff --git a/go.mod b/go.mod index d829762fbc..1c399a2883 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/fsnotify/fsnotify v1.7.0 github.com/google/cel-go v0.17.7 github.com/google/go-cmp v0.6.0 + github.com/google/go-containerregistry v0.19.1 github.com/itchyny/gojq v0.12.14 github.com/nxadm/tail v1.4.11 github.com/prometheus/client_golang v1.18.0 @@ -22,6 +23,7 @@ require ( github.com/wzshiming/ctc v1.2.3 github.com/wzshiming/easycel v0.5.0 github.com/wzshiming/getch v0.0.0-20201023133301-8e758c21cf27 + github.com/wzshiming/httpseek v0.0.0-20240122110938-0533c4b2d7c5 go.etcd.io/etcd/client/v3 v3.5.11 golang.org/x/sync v0.6.0 golang.org/x/sys v0.16.0 @@ -50,11 +52,16 @@ require ( github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230512164433-5d1fd1a340c9 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect github.com/containernetworking/cni v1.1.2 // indirect github.com/coreos/go-semver v0.3.1 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/docker/cli v24.0.0+incompatible // indirect + github.com/docker/distribution v2.8.2+incompatible // indirect + github.com/docker/docker v24.0.0+incompatible // indirect + github.com/docker/docker-credential-helpers v0.7.0 // indirect github.com/evanphx/json-patch v5.6.0+incompatible // indirect github.com/evanphx/json-patch/v5 v5.8.0 // indirect github.com/fatih/color v1.16.0 // indirect @@ -78,11 +85,13 @@ require ( github.com/itchyny/timefmt-go v0.1.5 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.16.5 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect github.com/mitchellh/copystructure v1.0.0 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/reflectwalk v1.0.0 // indirect github.com/moby/spdystream v0.2.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect @@ -90,14 +99,18 @@ require ( github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0-rc3 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/common v0.45.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect github.com/rogpeppe/go-internal v1.10.1-0.20230524175051-ec119421bb97 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/shopspring/decimal v1.2.0 // indirect + github.com/sirupsen/logrus v1.9.1 // indirect github.com/spf13/cast v1.3.1 // indirect github.com/stoewer/go-strcase v1.3.0 // indirect + github.com/vbatts/tar-split v0.11.3 // indirect github.com/vladimirvivien/gexe v0.2.0 // indirect github.com/wzshiming/trie v0.1.1 // indirect github.com/wzshiming/winseq v0.0.0-20200112104235-db357dc107ae // indirect diff --git a/go.sum b/go.sum index b64170cde5..eb1deb53cb 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,4 @@ +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g= @@ -19,6 +20,8 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/containerd/go-cni v1.1.9 h1:ORi7P1dYzCwVM6XPN4n3CbkuOx/NZ2DOqy+SHRdo9rU= github.com/containerd/go-cni v1.1.9/go.mod h1:XYrZJ1d5W6E2VOvjffL3IZq0Dz6bsVlERHbekNK90PM= +github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k= +github.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNAHhJogcZtrNHdKrA99/FCCRjE3HD36o= github.com/containernetworking/cni v1.1.2 h1:wtRGZVv7olUHMOqouPpn3cXJWpJgM6+EUl31EQbXALQ= github.com/containernetworking/cni v1.1.2/go.mod h1:sDpYKmGVENF3s6uvMvGgldDWeG8dMxakj/u+i9ht9vw= github.com/containernetworking/plugins v1.4.0 h1:+w22VPYgk7nQHw7KT92lsRmuToHvb7wwSv9iTbXzzic= @@ -27,6 +30,7 @@ github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -35,6 +39,14 @@ github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/docker/cli v24.0.0+incompatible h1:0+1VshNwBQzQAx9lOl+OYCTCEAD8fKs/qeXMx3O0wqM= +github.com/docker/cli v24.0.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= +github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v24.0.0+incompatible h1:z4bf8HvONXX9Tde5lGBMQ7yCJgNahmJumdrStZAbeY4= +github.com/docker/docker v24.0.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A= +github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0= github.com/emicklei/go-restful/v3 v3.11.2 h1:1onLa9DcsMYO9P+CXaL0dStDqQ2EHHXLiz+BtnqkLAU= github.com/emicklei/go-restful/v3 v3.11.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= @@ -95,6 +107,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-containerregistry v0.19.1 h1:yMQ62Al6/V0Z7CqIrrS1iYoA5/oQCm88DeNujc7C1KY= +github.com/google/go-containerregistry v0.19.1/go.mod h1:YCMFNQeeXeLF+dnhhWkqDItx/JSkH01j1Kis4PsjzFI= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= @@ -129,6 +143,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI= +github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -148,6 +164,8 @@ github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvls github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= @@ -180,6 +198,10 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8= github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0-rc3 h1:fzg1mXZFj8YdPeNkRXMg+zb88BFV0Ys52cJydRwBkb8= +github.com/opencontainers/image-spec v1.1.0-rc3/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -200,6 +222,9 @@ github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.1 h1:Ou41VVR3nMWWmTiEUnj0OlsgOSCUFgsPAOl6jRIcVtQ= +github.com/sirupsen/logrus v1.9.1/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= @@ -221,6 +246,9 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8= +github.com/vbatts/tar-split v0.11.3 h1:hLFqsOLQ1SsppQNTMpkpPXClLDfC2A3Zgy9OUU+RVck= +github.com/vbatts/tar-split v0.11.3/go.mod h1:9QlHN18E+fEH7RdG+QAJJcuya3rqT7eXSTY7wGrAokY= github.com/vladimirvivien/gexe v0.2.0 h1:nbdAQ6vbZ+ZNsolCgSVb9Fno60kzSuvtzVh6Ytqi/xY= github.com/vladimirvivien/gexe v0.2.0/go.mod h1:LHQL00w/7gDUKIak24n801ABp8C+ni6eBht9vGVst8w= github.com/wzshiming/cmux v0.3.3 h1:WlcKUwSN4vpClnHiyX9I4RtZ4xJeAqfrf4ltxSWuPoQ= @@ -231,6 +259,8 @@ github.com/wzshiming/easycel v0.5.0 h1:pnMBpOuEfr9EYLNpKD52NfpAhUrOQ3uAHFQT7bF76 github.com/wzshiming/easycel v0.5.0/go.mod h1:4Jue5gr86TomO6urajHxWCNbEKwwYnQVsyjKOWT/V/U= github.com/wzshiming/getch v0.0.0-20201023133301-8e758c21cf27 h1:jKLZ5tGpOZRid6GpBbDmP4BiooSF5zDd7Cg9jHCN4Yk= github.com/wzshiming/getch v0.0.0-20201023133301-8e758c21cf27/go.mod h1:TYK5eJtSD5cVebob9fE7D3jR1fbRU1EwkoPzbLWsOAk= +github.com/wzshiming/httpseek v0.0.0-20240122110938-0533c4b2d7c5 h1:SrtWdpPvx24qonvPvC0hO7ltJq4uHv+ASkMQ7Gmqh1g= +github.com/wzshiming/httpseek v0.0.0-20240122110938-0533c4b2d7c5/go.mod h1:YoZhlLIwNjTBDXIT8NpK5zRjOgZouRXPaBfjVXdqMMs= github.com/wzshiming/trie v0.1.1 h1:02AaBSZGhs6Aqljp8fz4xq/Mg8omFBPIlrUS0pJ11ks= github.com/wzshiming/trie v0.1.1/go.mod h1:c9thxXTh4KcGkejt4sUsO4c5GUmWpxeWzOJ7AZJaI+8= github.com/wzshiming/winseq v0.0.0-20200112104235-db357dc107ae h1:tpXvBXC3hpQBDCc9OojJZCQMVRAbT3TTdUMP8WguXkY= @@ -306,8 +336,10 @@ golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220906165534-d0df966e6959/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -381,6 +413,8 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= +gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= k8s.io/api v0.29.1 h1:DAjwWX/9YT7NQD4INu49ROJuZAAAP/Ijki48GUPzxqw= k8s.io/api v0.29.1/go.mod h1:7Kl10vBRUXhnQQI8YR/R327zXC8eJ7887/+Ybta+RoQ= k8s.io/apiextensions-apiserver v0.29.0 h1:0VuspFG7Hj+SxyF/Z/2T0uFbI5gb5LRgEyUVE3Q4lV0= diff --git a/pkg/kwokctl/cmd/create/cluster/cluster.go b/pkg/kwokctl/cmd/create/cluster/cluster.go index 711e8be653..d652b33a81 100644 --- a/pkg/kwokctl/cmd/create/cluster/cluster.go +++ b/pkg/kwokctl/cmd/create/cluster/cluster.go @@ -253,6 +253,10 @@ func runE(ctx context.Context, flags *flagpole) error { if err != nil { return fmt.Errorf("runtime %v not available: %w", flags.Options.Runtime, err) } + err = rt.Available(ctx) + if err != nil { + return fmt.Errorf("runtime %v not available: %w", flags.Options.Runtime, err) + } } // Set up the cluster diff --git a/pkg/kwokctl/runtime/compose/cluster.go b/pkg/kwokctl/runtime/compose/cluster.go index c81d073486..1365434433 100644 --- a/pkg/kwokctl/runtime/compose/cluster.go +++ b/pkg/kwokctl/runtime/compose/cluster.go @@ -78,6 +78,9 @@ func NewDockerCluster(name, workdir string) (runtime.Runtime, error) { // Available checks whether the runtime is available. func (c *Cluster) Available(ctx context.Context) error { + if c.IsDryRun() { + return nil + } return c.Exec(ctx, c.runtime, "version") } diff --git a/pkg/kwokctl/runtime/exec.go b/pkg/kwokctl/runtime/exec.go index 5eae78b798..686b9e2d31 100644 --- a/pkg/kwokctl/runtime/exec.go +++ b/pkg/kwokctl/runtime/exec.go @@ -19,8 +19,11 @@ package runtime import ( "bytes" "context" + "errors" "fmt" + "io" "os" + "path/filepath" "strconv" "strings" @@ -28,6 +31,7 @@ import ( "sigs.k8s.io/kwok/pkg/log" "sigs.k8s.io/kwok/pkg/utils/exec" "sigs.k8s.io/kwok/pkg/utils/file" + utilsimage "sigs.k8s.io/kwok/pkg/utils/image" "sigs.k8s.io/kwok/pkg/utils/path" "sigs.k8s.io/kwok/pkg/utils/version" ) @@ -137,18 +141,6 @@ func (c *Cluster) ForkExecIsRunning(ctx context.Context, dir string, name string return exec.IsRunning(pid) } -// PullImages is a helper function to pull images -func (c *Cluster) PullImages(ctx context.Context, command string, images []string, quiet bool) error { - if c.IsDryRun() { - for _, image := range images { - dryrun.PrintMessage("%s pull %s", command, image) - } - return nil - } - - return exec.PullImages(ctx, command, images, quiet) -} - // EnsureImage ensures the image exists. func (c *Cluster) EnsureImage(ctx context.Context, command string, image string) error { if c.IsDryRun() { @@ -162,7 +154,71 @@ func (c *Cluster) EnsureImage(ctx context.Context, command string, image string) } conf := config.Options - return exec.PullImage(ctx, command, image, conf.QuietPull) + logger := log.FromContext(ctx) + + err = exec.Exec(ctx, + command, "inspect", + image, + ) + if err == nil { + logger.Debug("Image already exists", + "image", image, + ) + return nil + } + + err = c.ensureImage(ctx, command, image, conf.QuietPull, conf.CacheDir) + if err != nil { + if ctx.Err() != nil { + return err + } + logger.Debug("Failed to pull", + "image", image, + "err", err, + ) + err0 := c.ensureImageWithRuntime(ctx, command, image, conf.QuietPull) + if err0 != nil { + return errors.Join(err, err0) + } + } + return nil +} + +func (c *Cluster) ensureImage(ctx context.Context, command string, image string, quiet bool, cacheDir string) error { + dest := path.Join(cacheDir, "tarball", image+".tar") + err := os.MkdirAll(filepath.Dir(dest), 0750) + if err != nil { + return err + } + cache := path.Join(cacheDir, "blobs") + err = utilsimage.Pull(ctx, cache, image, dest, quiet) + if err != nil { + return err + } + + err = exec.Exec(ctx, command, "load", + "-i", dest, + ) + if err != nil { + return err + } + + err = file.Remove(dest) + if err != nil { + logger := log.FromContext(ctx) + logger.Error("Remove file", err) + } + return nil +} + +func (c *Cluster) ensureImageWithRuntime(ctx context.Context, command string, image string, quiet bool) error { + var out io.Writer = os.Stderr + if quiet { + out = nil + } + return exec.Exec(exec.WithAllWriteTo(ctx, out), command, "pull", + image, + ) } // Exec executes the given command and returns the output. diff --git a/pkg/kwokctl/runtime/kind/cluster.go b/pkg/kwokctl/runtime/kind/cluster.go index bd0322a5be..497580d1c5 100644 --- a/pkg/kwokctl/runtime/kind/cluster.go +++ b/pkg/kwokctl/runtime/kind/cluster.go @@ -75,6 +75,9 @@ func NewPodmanCluster(name, workdir string) (runtime.Runtime, error) { // Available checks whether the runtime is available. func (c *Cluster) Available(ctx context.Context) error { + if c.IsDryRun() { + return nil + } return c.Exec(ctx, c.runtime, "version") } diff --git a/pkg/utils/exec/pull.go b/pkg/utils/exec/pull.go deleted file mode 100644 index b17bc084a8..0000000000 --- a/pkg/utils/exec/pull.go +++ /dev/null @@ -1,82 +0,0 @@ -/* -Copyright 2022 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 exec - -import ( - "context" - "io" - "os" - - "sigs.k8s.io/kwok/pkg/log" -) - -// PullImages is a helper function to pull images -func PullImages(ctx context.Context, command string, images []string, quiet bool) error { - var out io.Writer = os.Stderr - if quiet { - out = nil - } - - logger := log.FromContext(ctx) - - for _, image := range images { - err := Exec(ctx, - command, "inspect", - image, - ) - if err != nil { - logger.Info("Pull image", "image", image) - err = Exec(WithAllWriteTo(ctx, out), command, "pull", - image, - ) - if err != nil { - return err - } - } else { - logger.Debug("Image already exists", "image", image) - } - } - return nil -} - -// PullImage is a helper function to pull image -func PullImage(ctx context.Context, command string, image string, quiet bool) error { - var out io.Writer = os.Stderr - if quiet { - out = nil - } - - logger := log.FromContext(ctx) - - err := Exec(ctx, - command, "inspect", - image, - ) - if err != nil { - logger.Info("Pull image", "image", image) - err = Exec(WithAllWriteTo(ctx, out), command, "pull", - image, - ) - if err != nil { - return err - } - } else { - logger.Debug("Image already exists", "image", image) - } - - return nil -} diff --git a/pkg/utils/file/download.go b/pkg/utils/file/download.go index 15b971f9e2..4a0a5a9ea9 100644 --- a/pkg/utils/file/download.go +++ b/pkg/utils/file/download.go @@ -18,6 +18,7 @@ package file import ( "context" + "errors" "fmt" "io" "io/fs" @@ -25,10 +26,14 @@ import ( "net/url" "os" "path/filepath" - "strconv" + "time" + + "github.com/wzshiming/httpseek" "sigs.k8s.io/kwok/pkg/log" "sigs.k8s.io/kwok/pkg/utils/path" + "sigs.k8s.io/kwok/pkg/utils/progressbar" + "sigs.k8s.io/kwok/pkg/utils/version" ) // DownloadWithCacheAndExtract downloads the src file to the dest file, and extract it to the dest directory. @@ -142,14 +147,37 @@ func getCacheOrDownload(ctx context.Context, cacheDir, src string, mode fs.FileM case "http", "https": logger := log.FromContext(ctx) - logger.Info("Download", "uri", src) + logger = logger.With( + "uri", src, + ) + logger.Info("Download") + + var transport = http.DefaultTransport + var retry = 10 + transport = httpseek.NewMustReaderTransport(transport, func(req *http.Request, err error) error { + if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { + return err + } + if retry > 0 { + retry-- + time.Sleep(time.Second) + return nil + } + return err + }) - cli := &http.Client{} - req, err := http.NewRequest(http.MethodGet, u.String(), nil) + if !quiet { + transport = progressbar.NewTransport(transport) + } + + cli := &http.Client{ + Transport: transport, + } + req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) if err != nil { return "", err } - req = req.WithContext(ctx) + req.Header.Set("User-Agent", version.DefaultUserAgent()) resp, err := cli.Do(req) if err != nil { return "", err @@ -176,19 +204,7 @@ func getCacheOrDownload(ctx context.Context, cacheDir, src string, mode fs.FileM return "", err } - var srcReader io.Reader = resp.Body - if !quiet { - pb := newProgressBar() - contentLength := resp.Header.Get("Content-Length") - contentLengthInt, _ := strconv.Atoi(contentLength) - counter := newCounterWriter(func(counter int) { - pb.Update(counter, contentLengthInt) - pb.Print() - }) - srcReader = io.TeeReader(srcReader, counter) - } - - _, err = io.Copy(d, srcReader) + _, err = io.Copy(d, resp.Body) if err != nil { _ = d.Close() fmt.Println() @@ -208,19 +224,3 @@ func getCacheOrDownload(ctx context.Context, cacheDir, src string, mode fs.FileM return src, nil } } - -type counterWriter struct { - fun func(counter int) - counter int -} - -func newCounterWriter(fun func(counter int)) *counterWriter { - return &counterWriter{ - fun: fun, - } -} -func (c *counterWriter) Write(b []byte) (int, error) { - c.counter += len(b) - c.fun(c.counter) - return len(b), nil -} diff --git a/pkg/utils/file/progress_bar.go b/pkg/utils/file/progress_bar.go deleted file mode 100644 index 68fcd86828..0000000000 --- a/pkg/utils/file/progress_bar.go +++ /dev/null @@ -1,60 +0,0 @@ -/* -Copyright 2022 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 file - -import ( - "fmt" - "os" - "strings" - "time" -) - -type progressBar struct { - total int - current int - lastUpdateTime time.Time - startTime time.Time -} - -func newProgressBar() *progressBar { - return &progressBar{ - startTime: time.Now(), - } -} - -func (p *progressBar) Update(current, total int) { - p.current = current - p.total = total -} - -func (p *progressBar) Print() { - if p.total == 0 { - return - } - now := time.Now() - if p.current < p.total && - now.Sub(p.lastUpdateTime) < time.Second/10 { - return - } - p.lastUpdateTime = now - - if p.current >= p.total { - _, _ = fmt.Fprintf(os.Stderr, "\r%-60s| 100%% %-5s\n", strings.Repeat("#", 60), time.Since(p.startTime).Truncate(time.Second)) - } else { - _, _ = fmt.Fprintf(os.Stderr, "\r%-60s| %.1f%% %-5s", strings.Repeat("#", int(float64(p.current)/float64(p.total)*60)), float64(p.current)/float64(p.total)*100, time.Since(p.startTime).Truncate(time.Second)) - } -} diff --git a/pkg/utils/image/doc.go b/pkg/utils/image/doc.go new file mode 100644 index 0000000000..cfd457cdf7 --- /dev/null +++ b/pkg/utils/image/doc.go @@ -0,0 +1,18 @@ +/* +Copyright 2024 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 image provides utilities for working with container images. +package image diff --git a/pkg/utils/image/pull.go b/pkg/utils/image/pull.go new file mode 100644 index 0000000000..98586c83a4 --- /dev/null +++ b/pkg/utils/image/pull.go @@ -0,0 +1,99 @@ +/* +Copyright 2024 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 image + +import ( + "context" + "errors" + "fmt" + "net/http" + "runtime" + "time" + + "github.com/google/go-containerregistry/pkg/crane" + "github.com/google/go-containerregistry/pkg/name" + containerregistryv1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/cache" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/wzshiming/httpseek" + + "sigs.k8s.io/kwok/pkg/log" + "sigs.k8s.io/kwok/pkg/utils/progressbar" + "sigs.k8s.io/kwok/pkg/utils/version" +) + +// Pull pulls an image from a registry. +func Pull(ctx context.Context, cacheDir, src, dest string, quiet bool) error { + logger := log.FromContext(ctx) + logger = logger.With( + "image", src, + ) + logger.Info("Pull") + + var transport = remote.DefaultTransport + var retry = 10 + transport = httpseek.NewMustReaderTransport(transport, func(req *http.Request, err error) error { + if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { + return err + } + if retry > 0 { + retry-- + time.Sleep(time.Second) + return nil + } + return err + }) + + if !quiet { + transport = progressbar.NewTransport(transport) + } + + o := crane.GetOptions( + crane.WithContext(ctx), + crane.WithUserAgent(version.DefaultUserAgent()), + crane.WithTransport(transport), + crane.WithPlatform(&containerregistryv1.Platform{ + OS: "linux", + Architecture: runtime.GOARCH, + }), + ) + + ref, err := name.ParseReference(src, o.Name...) + if err != nil { + return fmt.Errorf("parsing reference %q: %w", src, err) + } + + rmt, err := remote.Get(ref, o.Remote...) + if err != nil { + return err + } + + img, err := rmt.Image() + if err != nil { + return err + } + if cacheDir != "" { + img = cache.Image(img, cache.NewFilesystemCache(cacheDir)) + } + + err = crane.Save(img, src, dest) + if err != nil { + return fmt.Errorf("saving tarball %s: %w", dest, err) + } + + return nil +} diff --git a/pkg/utils/progressbar/doc.go b/pkg/utils/progressbar/doc.go new file mode 100644 index 0000000000..33f592cea6 --- /dev/null +++ b/pkg/utils/progressbar/doc.go @@ -0,0 +1,18 @@ +/* +Copyright 2024 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 progressbar provides a transport that writes a progress bar to out. +package progressbar diff --git a/pkg/utils/progressbar/format.go b/pkg/utils/progressbar/format.go new file mode 100644 index 0000000000..a7c3043c99 --- /dev/null +++ b/pkg/utils/progressbar/format.go @@ -0,0 +1,103 @@ +/* +Copyright 2024 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 progressbar + +import ( + "fmt" + "strings" + "time" + + "github.com/wzshiming/ctc" + + "sigs.k8s.io/kwok/pkg/utils/width" +) + +func formatDownloadProgress(name string, max uint64, current uint64, total uint64, elapsed time.Duration) string { + var per string + if current == total { + per = fmt.Sprintf("size=%s", formatBytes(total)) + } else { + per = fmt.Sprintf("size=%s/%s", formatBytes(current), formatBytes(total)) + } + + e := elapsed.Truncate(time.Second) + if e != 0 || current != total { + widePer := fmt.Sprintf("%s speed=%s elapsed=%s", per, formatSpeed(current, elapsed), e) + if len(widePer) < int(max)-1 { + per = widePer + } + } + + return formatBar(max, name, per, current, total) +} + +func formatBar(max uint64, preInfo, postInfo string, current uint64, total uint64) string { + preInfoWidth := width.Width(preInfo) + postInfoWidth := width.Width(postInfo) + infoWidth := preInfoWidth + postInfoWidth + if infoWidth > int(max) { + preInfoWidth = int(max) - postInfoWidth - 1 + preInfo = width.Shorten(preInfo, preInfoWidth) + } + barInfo := strings.Join([]string{ + preInfo, + strings.Repeat(" ", int(max)-preInfoWidth-postInfoWidth), + postInfo, + }, "") + + count := current * max / total + if count == max { + return barInfo + } + + barInfoRunes := []rune(barInfo) + bar := strings.Join([]string{ + ctc.Reset.String(), + string(barInfoRunes[:count]), + ctc.Negative.String(), + string(barInfoRunes[count:]), + ctc.Reset.String(), + }, "") + return bar +} + +func formatSpeed(size uint64, elapsed time.Duration) string { + second := elapsed.Seconds() + if second < 1 { + return formatBytes(size) + "/s" + } + return formatBytes(uint64(float64(size)/second)) + "/s" +} + +var ( + binaryAbbrs = []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"} +) + +func formatBytes(size uint64) string { + s, u := getSizeAndUnit(float64(size), 1024, binaryAbbrs) + return fmt.Sprintf("%.0f%s", s, u) +} + +func getSizeAndUnit(size float64, base float64, abbrs []string) (float64, string) { + i := 0 + unitsLimit := len(abbrs) - 1 + for size >= base && i < unitsLimit { + size = size / base + i++ + } + return size, abbrs[i] +} diff --git a/pkg/utils/progressbar/progressbar.go b/pkg/utils/progressbar/progressbar.go new file mode 100644 index 0000000000..8832de9fc9 --- /dev/null +++ b/pkg/utils/progressbar/progressbar.go @@ -0,0 +1,80 @@ +/* +Copyright 2024 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 progressbar + +import ( + "io" + "net/http" + "path" + "strconv" + "strings" +) + +// NewTransport returns a new transport that writes a progress bar to out. +func NewTransport(base http.RoundTripper) http.RoundTripper { + return &transport{ + base: base, + } +} + +type transport struct { + base http.RoundTripper +} + +func (p *transport) RoundTrip(req *http.Request) (*http.Response, error) { + resp, err := p.base.RoundTrip(req) + if err != nil { + return nil, err + } + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return resp, nil + } + + if strings.HasSuffix(req.URL.Path, "/") { + return resp, nil + } + + contentLength := resp.Header.Get("Content-Length") + if contentLength == "" { + return resp, nil + } + + contentLengthInt, _ := strconv.Atoi(contentLength) + if contentLengthInt <= 0 { + return resp, nil + } + + var name string + ref := req.Referer() + if ref != "" { + name = path.Base(ref) + } else { + name = path.Base(req.URL.Path) + } + + body := struct { + io.Reader + io.Closer + }{ + Reader: NewReader(resp.Body, name, uint64(contentLengthInt)), + Closer: resp.Body, + } + resp.Body = body + + return resp, nil +} diff --git a/pkg/utils/progressbar/reader.go b/pkg/utils/progressbar/reader.go new file mode 100644 index 0000000000..87297ad18a --- /dev/null +++ b/pkg/utils/progressbar/reader.go @@ -0,0 +1,70 @@ +/* +Copyright 2024 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 progressbar + +import ( + "io" + "os" + + "golang.org/x/term" + "time" +) + +type reader struct { + reader io.Reader + current uint64 + + name string + total uint64 + + startTime time.Time + lastUpdateTime time.Time +} + +// NewReader returns a new reader that writes a progress bar to out. +func NewReader(r io.Reader, name string, total uint64) io.Reader { + return &reader{ + reader: r, + name: name, + total: total, + startTime: time.Now(), + } +} + +func (r *reader) Read(b []byte) (int, error) { + n, err := r.reader.Read(b) + if n == 0 { + return n, err + } + r.current += uint64(n) + + if r.current != r.total && time.Since(r.lastUpdateTime) < time.Second*10 { + return n, err + } + + out := os.Stderr + termWidth, _, _ := term.GetSize(int(out.Fd())) + if termWidth > 0 { + info := formatDownloadProgress(r.name, uint64(termWidth), r.current, r.total, time.Since(r.startTime)) + if r.current == r.total { + _, _ = out.WriteString("\r" + info + "\n") + } else { + _, _ = out.WriteString("\r" + info) + } + } + return n, err +} diff --git a/pkg/utils/width/doc.go b/pkg/utils/width/doc.go new file mode 100644 index 0000000000..9fa6bf5c7f --- /dev/null +++ b/pkg/utils/width/doc.go @@ -0,0 +1,18 @@ +/* +Copyright 2024 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 width provides functions to measure the width of a string. +package width diff --git a/pkg/utils/width/width.go b/pkg/utils/width/width.go new file mode 100644 index 0000000000..9e7f4c58aa --- /dev/null +++ b/pkg/utils/width/width.go @@ -0,0 +1,73 @@ +/* +Copyright 2024 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 width + +import ( + "unicode/utf8" +) + +// Width returns the width of a string. +func Width(str string) int { + n := 0 + for _, r := range str { + n += runeWidth(r) + } + return n +} + +func runeWidth(r rune) int { + switch { + case r == utf8.RuneError || r < '\x20': + return 0 + case '\x20' <= r && r < '\u2000': + return 1 + case '\u2000' <= r && r < '\uFF61': + return 2 + case '\uFF61' <= r && r < '\uFFA0': + return 1 + case '\uFFA0' <= r: + return 2 + } + return 0 +} + +// Shorten returns a shortened string. +func Shorten(str string, max int) string { + if Width(str) <= max { + return str + } + + runes := []rune(str) + begin := 0 + end := len(runes) - 1 + w := 0 + for i := 0; i < len(runes)/2; i++ { + w += runeWidth(runes[begin]) + if w >= max-2 { + break + } + begin++ + + w += runeWidth(runes[end]) + if w >= max-2 { + break + } + end-- + } + + return string(append(runes[:begin], append([]rune("..."), runes[end+1:]...)...)) +} diff --git a/pkg/utils/width/width_test.go b/pkg/utils/width/width_test.go new file mode 100644 index 0000000000..593f465952 --- /dev/null +++ b/pkg/utils/width/width_test.go @@ -0,0 +1,70 @@ +/* +Copyright 2024 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 width + +import ( + "fmt" + "testing" +) + +func TestShorten(t *testing.T) { + type args struct { + str string + max int + } + tests := []struct { + args args + want string + }{ + { + args: args{ + str: "hello world", + max: 5, + }, + want: "h...d", + }, + { + args: args{ + str: "hello world", + max: 6, + }, + want: "he...d", + }, + { + args: args{ + str: "hello world!", + max: 5, + }, + want: "h...!", + }, + { + args: args{ + str: "hello world!", + max: 6, + }, + want: "he...!", + }, + } + for _, tt := range tests { + name := fmt.Sprintf("Shorten(%s, %d)", tt.args.str, tt.args.max) + t.Run(name, func(t *testing.T) { + if got := Shorten(tt.args.str, tt.args.max); got != tt.want { + t.Errorf("Shorten() = %v, want %v", got, tt.want) + } + }) + } +}