mirror of
https://github.com/project-zot/zot.git
synced 2024-12-16 21:56:37 -05:00
cli: add config and images command
Extends the existing zot CLI to add commands for listing all images and their details on a zot server. Listing all images introduces the need for configurations. Each configuration has a name and URL at the least. Check 'zot config -h' for more details. The user can specify the URL of zot server explicitly while running the command or configure a URL and pass it directly. Adding a configuration: zot config add aci-zot <zot-url> Run 'zot config --help' for more. Listing all images: zot images --url <zot-url> Pass a config instead of the url: zot images <config-name> Filter the list of images by image name: zot images <config-name> --name <image-name> Run 'zot images --help' for all details - Stores configurations in '$HOME/.zot' file Add CLI README
This commit is contained in:
parent
4a1519bb1d
commit
ad684ac44b
17 changed files with 2169 additions and 84 deletions
39
README.md
39
README.md
|
@ -7,6 +7,7 @@
|
|||
* Uses [OCI storage layout](https://github.com/opencontainers/image-spec/blob/master/image-layout.md) for storage layout
|
||||
* Supports [helm charts](https://helm.sh/docs/topics/registries/)
|
||||
* Currently suitable for on-prem deployments (e.g. colocated with Kubernetes)
|
||||
* [Command-line client support](#cli)
|
||||
* TLS support
|
||||
* Authentication via:
|
||||
* TLS mutual authentication
|
||||
|
@ -97,6 +98,44 @@ podman run --rm -p 8080:8080 \
|
|||
zot:latest
|
||||
```
|
||||
|
||||
# CLI
|
||||
|
||||
The same zot binary can be used for interacting with any zot server instances.
|
||||
|
||||
## Adding a zot server URL
|
||||
|
||||
To add a zot server URL with an alias "remote-zot":
|
||||
|
||||
```console
|
||||
$ zot config add remote-zot https://server-example:8080
|
||||
```
|
||||
|
||||
List all configured URLs with their aliases:
|
||||
```console
|
||||
$ zot config -l
|
||||
remote-zot https://server-example:8080
|
||||
local http://localhost:8080
|
||||
```
|
||||
|
||||
## Fetching images
|
||||
You can fetch all images from a server by using its alias specified [in this step](#adding-a-zot-server-url):
|
||||
|
||||
```console
|
||||
$ zot images remote-zot
|
||||
IMAGE NAME TAG DIGEST SIZE
|
||||
postgres 9.6.18-alpine ef27f3e1 14.4MB
|
||||
postgres 9.5-alpine 264450a7 14.4MB
|
||||
busybox latest 414aeb86 707.8KB
|
||||
```
|
||||
|
||||
Or filter the list by an image name:
|
||||
|
||||
```console
|
||||
$ zot images remote-zot -n busybox
|
||||
IMAGE NAME TAG DIGEST SIZE
|
||||
busybox latest 414aeb86 707.8KB
|
||||
```
|
||||
|
||||
# Ecosystem
|
||||
|
||||
Since we couldn't find clients or client libraries that are stictly compliant to
|
||||
|
|
57
WORKSPACE
57
WORKSPACE
|
@ -461,8 +461,8 @@ go_repository(
|
|||
go_repository(
|
||||
name = "com_github_kr_pty",
|
||||
importpath = "github.com/kr/pty",
|
||||
sum = "h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI=",
|
||||
version = "v1.1.8",
|
||||
sum = "h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw=",
|
||||
version = "v1.1.1",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
|
@ -881,8 +881,8 @@ go_repository(
|
|||
go_repository(
|
||||
name = "org_golang_google_appengine",
|
||||
importpath = "google.golang.org/appengine",
|
||||
sum = "h1:igQkv0AAhEIvTEpD5LIpAfav2eeVO9HBTjvKHVJPRSs=",
|
||||
version = "v1.1.0",
|
||||
sum = "h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=",
|
||||
version = "v1.4.0",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
|
@ -1521,3 +1521,52 @@ go_repository(
|
|||
sum = "h1:UhZDfRO8JRQru4/+LlLE0BRKGF8L+PICnvYZmx/fEGA=",
|
||||
version = "v1.24.0",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_briandowns_spinner",
|
||||
importpath = "github.com/briandowns/spinner",
|
||||
sum = "h1:OixPqDEcX3juo5AjQZAnFPbeUA0jvkp2qzB5gOZJ/L0=",
|
||||
version = "v1.11.1",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_coreos_go_etcd",
|
||||
importpath = "github.com/coreos/go-etcd",
|
||||
sum = "h1:bXhRBIXoTm9BYHS3gE0TtQuyNZyeEMux2sDi4oo5YOo=",
|
||||
version = "v2.0.0+incompatible",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_dustin_go_humanize",
|
||||
importpath = "github.com/dustin/go-humanize",
|
||||
sum = "h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=",
|
||||
version = "v1.0.0",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_go_resty_resty_v2",
|
||||
importpath = "github.com/go-resty/resty/v2",
|
||||
sum = "h1:JOOeAvjSlapTT92p8xiS19Zxev1neGikoHsXJeOq8So=",
|
||||
version = "v2.3.0",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_mattn_go_runewidth",
|
||||
importpath = "github.com/mattn/go-runewidth",
|
||||
sum = "h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54=",
|
||||
version = "v0.0.7",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_olekukonko_tablewriter",
|
||||
importpath = "github.com/olekukonko/tablewriter",
|
||||
sum = "h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8=",
|
||||
version = "v0.0.4",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "org_golang_x_exp",
|
||||
importpath = "golang.org/x/exp",
|
||||
sum = "h1:c2HOrn5iMezYjSlGPncknSEr/8x5LELb/ilJbXi9DEA=",
|
||||
version = "v0.0.0-20190121172915-509febef88a4",
|
||||
)
|
||||
|
|
|
@ -2,12 +2,20 @@ package main
|
|||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/anuvu/zot/pkg/cli"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := cli.NewRootCmd().Execute(); err != nil {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
configPath := path.Join(home + "/.zot")
|
||||
|
||||
if err := cli.NewRootCmd(configPath).Execute(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package main_test
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/anuvu/zot/pkg/api"
|
||||
|
@ -14,7 +15,8 @@ func TestIntegration(t *testing.T) {
|
|||
c := api.NewController(config)
|
||||
So(c, ShouldNotBeNil)
|
||||
|
||||
cl := cli.NewRootCmd()
|
||||
tempFile, _ := ioutil.TempFile("", "tmp-")
|
||||
cl := cli.NewRootCmd(tempFile.Name())
|
||||
So(cl, ShouldNotBeNil)
|
||||
|
||||
So(cl.Execute(), ShouldBeNil)
|
||||
|
|
|
@ -26,4 +26,12 @@ var (
|
|||
ErrCacheMiss = errors.New("cache: miss")
|
||||
ErrRequireCred = errors.New("ldap: bind credentials required")
|
||||
ErrInvalidCred = errors.New("ldap: invalid credentials")
|
||||
ErrInvalidArgs = errors.New("cli: Invalid Arguments")
|
||||
ErrInvalidFlagsCombination = errors.New("cli: Invalid combination of flags. Add --help flag")
|
||||
ErrInvalidURL = errors.New("cli: invalid URL format")
|
||||
ErrUnauthorizedAccess = errors.New("cli: unauthorized access. check credentials")
|
||||
ErrCannotResetConfigKey = errors.New("cli: cannot reset given config key")
|
||||
ErrConfigNotFound = errors.New("cli: config with the given name does not exist")
|
||||
ErrNoURLProvided = errors.New("cli: no URL provided in argument or via config. see 'zot config -h'")
|
||||
ErrIllegalConfigKey = errors.New("cli: given config key is not allowed")
|
||||
)
|
||||
|
|
4
go.mod
4
go.mod
|
@ -4,7 +4,9 @@ go 1.14
|
|||
|
||||
require (
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751
|
||||
github.com/briandowns/spinner v1.11.1
|
||||
github.com/chartmuseum/auth v0.4.0
|
||||
github.com/dustin/go-humanize v1.0.0
|
||||
github.com/getlantern/deepcopy v0.0.0-20160317154340-7f45deb8130a
|
||||
github.com/go-chi/chi v4.0.2+incompatible // indirect
|
||||
github.com/go-ldap/ldap/v3 v3.1.3
|
||||
|
@ -15,6 +17,7 @@ require (
|
|||
github.com/mitchellh/mapstructure v1.1.2
|
||||
github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484 // indirect
|
||||
github.com/nmcclain/ldap v0.0.0-20191021200707-3b3b69a7e9e3
|
||||
github.com/olekukonko/tablewriter v0.0.4
|
||||
github.com/opencontainers/distribution-spec v1.0.0-rc0
|
||||
github.com/opencontainers/go-digest v1.0.0
|
||||
github.com/opencontainers/image-spec v1.0.1
|
||||
|
@ -29,4 +32,5 @@ require (
|
|||
go.etcd.io/bbolt v1.3.4
|
||||
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9
|
||||
gopkg.in/resty.v1 v1.12.0
|
||||
gopkg.in/yaml.v2 v2.2.4
|
||||
)
|
||||
|
|
65
go.sum
65
go.sum
|
@ -12,20 +12,18 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy
|
|||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/apex/log v1.1.1 h1:BwhRZ0qbjYtTob0I+2M+smavV0kOC8XgcnGZcyL9liA=
|
||||
github.com/apex/log v1.1.1/go.mod h1:Ls949n1HFtXfbDcjiTTFQqkVUrte0puoIBfO3SVgwOA=
|
||||
github.com/apex/log v1.4.0 h1:jYWeNt9kWJOf1ifht8UjsCQ00eiPnFrUzCBCiiJMw/g=
|
||||
github.com/apex/log v1.4.0/go.mod h1:UMNC4vQNC7hb5gyr47r18ylK1n34rV7GO+gb0wpXvcE=
|
||||
github.com/apex/logs v0.0.7/go.mod h1:XzxuLZ5myVHDy9SAmYpamKKRNApGj54PfYLcFrXqDwo=
|
||||
github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a/go.mod h1:3NqKYiepwy8kCu4PNA+aP7WUV72eXWJeP9/r3/K9aLE=
|
||||
github.com/aphistic/sweet v0.2.0/go.mod h1:fWDlIh/isSE9n6EPsRmC0det+whmX6dJid3stzu0Xys=
|
||||
github.com/aphistic/sweet v0.3.0/go.mod h1:fWDlIh/isSE9n6EPsRmC0det+whmX6dJid3stzu0Xys=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
||||
github.com/aws/aws-sdk-go v1.23.21/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
||||
github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/briandowns/spinner v1.11.1 h1:OixPqDEcX3juo5AjQZAnFPbeUA0jvkp2qzB5gOZJ/L0=
|
||||
github.com/briandowns/spinner v1.11.1/go.mod h1:QOuQk7x+EaDASo80FEXwlwiA+j/PPIcX3FScO+3/ZPQ=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/chartmuseum/auth v0.4.0 h1:CkCJPO/daho9iN9t6ztK4cJRjHkQoom5/n5ndAS3OyM=
|
||||
|
@ -43,7 +41,6 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSY
|
|||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/cyphar/filepath-securejoin v0.2.2 h1:jCwT2GTP+PY5nBz3c/YL5PAIbusElVrPujOBSCj8xRg=
|
||||
github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4=
|
||||
|
@ -55,9 +52,12 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm
|
|||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
|
||||
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
|
||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
|
@ -88,6 +88,7 @@ github.com/go-openapi/spec v0.19.0 h1:A4SZ6IWh3lnjH0rG0Z5lkxazMGBECtrZcbyYQi+64k
|
|||
github.com/go-openapi/spec v0.19.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
|
||||
github.com/go-openapi/swag v0.17.0 h1:iqrgMg7Q7SvtbWLlltPrkMs0UBJI6oTSs79JFRUi880=
|
||||
github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
|
||||
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
|
||||
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
|
@ -113,8 +114,10 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a
|
|||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
|
@ -143,28 +146,21 @@ github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfV
|
|||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.8.3 h1:CkLseiEYMM/fRb0RIg9mXB+Iwgmle+U9KGFu+JCO4Ec=
|
||||
github.com/klauspost/compress v1.8.3/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/compress v1.10.9 h1:pPRt1Z78crspaHISkpSSHjDlx+Tt9suHe519dsI0vF4=
|
||||
github.com/klauspost/compress v1.10.9/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/klauspost/cpuid v1.2.1 h1:vJi+O/nMdFt0vqm8NZBI6wzALWdA2X+egi0ogNyrC/w=
|
||||
github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/klauspost/pgzip v1.2.1 h1:oIPZROsWuPHpOdMVWLuJZXwgjhrW8r1yEX8UqMyeNHM=
|
||||
github.com/klauspost/pgzip v1.2.1/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||
github.com/klauspost/pgzip v1.2.4 h1:TQ7CNpYKovDOmqzRHKxJh0BeaBI7UdQZYc6p7pMQh1A=
|
||||
github.com/klauspost/pgzip v1.2.4/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
|
||||
|
@ -172,16 +168,20 @@ github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czP
|
|||
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329 h1:2gxZ0XQIU/5z3Z3bUBu+FXuk2pFbkN6tcwi/pjyaDic=
|
||||
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
||||
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE=
|
||||
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54=
|
||||
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
|
@ -196,26 +196,23 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb
|
|||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484 h1:D9EvfGQvlkKaDr2CRKN++7HbSXbefUNDrPq60T+g24s=
|
||||
github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484/go.mod h1:O1EljZ+oHprtxDDPHiMWVo/5dBT6PlvWX5PSwj80aBA=
|
||||
github.com/nmcclain/ldap v0.0.0-20191021200707-3b3b69a7e9e3 h1:NNis9uuNpG5h97Dvxxo53Scg02qBg+3Nfabg6zjFGu8=
|
||||
github.com/nmcclain/ldap v0.0.0-20191021200707-3b3b69a7e9e3/go.mod h1:YtrVB1/v9Td9SyjXpjYVmbdKgj9B0nPTBsdGUxy0i8U=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8=
|
||||
github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/opencontainers/distribution-spec v1.0.0-rc0 h1:xMzwhweo1gjvEo74mQjGTLau0TD3ACyTEC1310NbuSQ=
|
||||
github.com/opencontainers/distribution-spec v1.0.0-rc0/go.mod h1:copR2flp+jTEvQIFMb6MIx45OkrxzqyjszPDT3hx/5Q=
|
||||
github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ=
|
||||
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
|
||||
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.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
|
||||
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
|
||||
github.com/opencontainers/runtime-spec v1.0.1 h1:wY4pOY8fBdSIvs9+IDHC55thBuEulhzfSgKeC1yFvzQ=
|
||||
github.com/opencontainers/runtime-spec v1.0.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
|
||||
github.com/opencontainers/runtime-spec v1.0.2 h1:UfAcuLBJB9Coz72x1hgl8O5RVzTdNiaglX6v2DM6FI0=
|
||||
github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
|
||||
github.com/opencontainers/umoci v0.4.6 h1:nUULYM+jSLLJCVN2gd4wldm8/yuVMahC36UXna3jEqI=
|
||||
|
@ -243,7 +240,6 @@ github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7z
|
|||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
github.com/rootless-containers/proto v0.1.0 h1:gS1JOMEtk1YDYHCzBAf/url+olMJbac7MTrgSeP6zh4=
|
||||
github.com/rootless-containers/proto v0.1.0/go.mod h1:vgkUFZbQd0gcE/K/ZwtE4MYjZPu0UNHLXIQxhyqAFh8=
|
||||
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||
|
@ -258,21 +254,16 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5I
|
|||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/assertions v1.0.0 h1:UVQPSSmc3qtTi+zPPkCXvZX9VvW/xT/NsRvKfwY81a8=
|
||||
github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM=
|
||||
github.com/smartystreets/assertions v1.0.1 h1:voD4ITNjPL5jjBfgR/r8fPIIBrliWrWHeiJApdr3r4w=
|
||||
github.com/smartystreets/assertions v1.0.1/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM=
|
||||
github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM=
|
||||
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/smartystreets/gunit v1.0.0/go.mod h1:qwPWnhz6pn0NnRBP++URONOVyNkPyr4SauJk4cUOwJs=
|
||||
github.com/smartystreets/gunit v1.0.4/go.mod h1:EH5qMBab2UclzXUcpR8b93eHsIlp9u+pDQIRp5DZNzQ=
|
||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
|
||||
|
@ -290,12 +281,12 @@ github.com/spf13/viper v1.6.1 h1:VPZzIkznI1YhVMRi6vNFLHSwhnhReBfgTxIPccpfdZk=
|
|||
github.com/spf13/viper v1.6.1/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
|
||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||
|
@ -308,6 +299,7 @@ github.com/swaggo/swag v1.5.1/go.mod h1:1Bl9F/ZBpVWh22nY0zmYyASPO1lI/zIwRDrpZU+t
|
|||
github.com/swaggo/swag v1.6.3 h1:N+uVPGP4H2hXoss2pt5dctoSUPKKRInr6qcTMOm0usI=
|
||||
github.com/swaggo/swag v1.6.3/go.mod h1:wcc83tB4Mb2aNiL/HP4MFeQdpHUrca+Rp/DRNgWAUio=
|
||||
github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0=
|
||||
github.com/tj/assert v0.0.3 h1:Df/BlaZ20mq6kuai7f5z2TvPFiwC3xaWJSDQNiIS3Rk=
|
||||
github.com/tj/assert v0.0.3/go.mod h1:Ne6X72Q+TB1AteidzQncjw9PabbMp4PBMZ1k+vd1Pvk=
|
||||
github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0=
|
||||
github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao=
|
||||
|
@ -319,12 +311,8 @@ github.com/ugorji/go/codec v0.0.0-20181022190402-e5e69e061d4f/go.mod h1:VFNgLljT
|
|||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/ugorji/go/codec v1.1.5-pre/go.mod h1:tULtS6Gy1AE1yCENaw4Vb//HLH5njI2tfCQDUqRd8fI=
|
||||
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||
github.com/urfave/cli v1.22.1 h1:+mkCCcOFKPnCmVYVcURKps1Xe+3zP90gSYGNfRkjoIY=
|
||||
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/urfave/cli v1.22.4 h1:u7tSpNPPswAFymm8IehJhy4uJMlUuU/GmqSkvJ1InXA=
|
||||
github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/vbatts/go-mtree v0.4.4 h1:+CncqETnSpxBCCUhRnNQBvxhsjWXNuc+ExZsLSNaj5o=
|
||||
github.com/vbatts/go-mtree v0.4.4/go.mod h1:3sazBqLG4bZYmgRTgdh9X3iKTzwBpp5CrREJDzrNSXY=
|
||||
github.com/vbatts/go-mtree v0.5.0 h1:dM+5XZdqH0j9CSZeerhoN/tAySdwnmevaZHO1XGW2Vc=
|
||||
github.com/vbatts/go-mtree v0.5.0/go.mod h1:7JbaNHyBMng+RP8C3Q4E+4Ca8JnGQA2R/MB+jb4tSOk=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
|
@ -340,10 +328,7 @@ golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnf
|
|||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 h1:ULYEB3JvPRE/IfO+9uO7vKV/xzVTO7XPAwm8xbf4w2g=
|
||||
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 h1:vEg9joUBmeBcK9iSJftGNf3coIG4HqZElCPehJsfAYM=
|
||||
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
|
@ -364,8 +349,6 @@ golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR
|
|||
golang.org/x/net v0.0.0-20190611141213-3f473d35a33a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190912160710-24e19bdeb0f2 h1:4dVFTC832rPn4pomLSz1vA+are2+dU19w1H8OngV7nc=
|
||||
golang.org/x/net v0.0.0-20190912160710-24e19bdeb0f2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYcn15TUbkhwuCO3foqqM=
|
||||
golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
|
@ -373,7 +356,6 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ
|
|||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
|
@ -387,8 +369,6 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190610200419-93c9922d18ae h1:xiXzMMEQdQcric9hXtr1QU98MHunKK7OTtsoU6bYWs4=
|
||||
golang.org/x/sys v0.0.0-20190610200419-93c9922d18ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190913121621-c3b328c6e5a7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
@ -412,9 +392,8 @@ golang.org/x/tools v0.0.0-20190606050223-4d9ae51c2468/go.mod h1:/rFqwRUd4F7ZHNgw
|
|||
golang.org/x/tools v0.0.0-20190611222205-d73e1c7e250b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74 h1:4cFkmztxtMslUX2SctSl+blCyXfpzhGOy9LhKAqSMA4=
|
||||
golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190913181337-0240832f5c3d h1:JG5eBjADEFhCRMAdfzvIYKuT/2E9oOzCDJMqMxD5LVw=
|
||||
golang.org/x/tools v0.0.0-20190913181337-0240832f5c3d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
|
@ -441,6 +420,7 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33
|
|||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
|
||||
|
@ -457,6 +437,7 @@ gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
|
|||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
|
|
|
@ -2,26 +2,50 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
|
|||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["root.go"],
|
||||
srcs = [
|
||||
"client.go",
|
||||
"config_cmd.go",
|
||||
"image_cmd.go",
|
||||
"root.go",
|
||||
"searcher.go",
|
||||
"service.go",
|
||||
],
|
||||
importpath = "github.com/anuvu/zot/pkg/cli",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//errors:go_default_library",
|
||||
"//pkg/api:go_default_library",
|
||||
"//pkg/storage:go_default_library",
|
||||
"@com_github_briandowns_spinner//:go_default_library",
|
||||
"@com_github_dustin_go_humanize//:go_default_library",
|
||||
"@com_github_json_iterator_go//:go_default_library",
|
||||
"@com_github_mitchellh_mapstructure//:go_default_library",
|
||||
"@com_github_olekukonko_tablewriter//:go_default_library",
|
||||
"@com_github_opencontainers_distribution_spec//:go_default_library",
|
||||
"@com_github_rs_zerolog//log:go_default_library",
|
||||
"@com_github_spf13_cobra//:go_default_library",
|
||||
"@com_github_spf13_viper//:go_default_library",
|
||||
"@in_gopkg_yaml_v2//:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
timeout = "short",
|
||||
srcs = ["root_test.go"],
|
||||
srcs = [
|
||||
"config_cmd_test.go",
|
||||
"image_cmd_test.go",
|
||||
"root_test.go",
|
||||
],
|
||||
embed = [":go_default_library"],
|
||||
race = "on",
|
||||
deps = ["@com_github_smartystreets_goconvey//convey:go_default_library"],
|
||||
deps = [
|
||||
"//errors:go_default_library",
|
||||
"//pkg/api:go_default_library",
|
||||
"//pkg/compliance/v1_0_0:go_default_library",
|
||||
"@com_github_opencontainers_go_digest//:go_default_library",
|
||||
"@com_github_opencontainers_image_spec//specs-go/v1:go_default_library",
|
||||
"@com_github_smartystreets_goconvey//convey:go_default_library",
|
||||
"@in_gopkg_resty_v1//:go_default_library",
|
||||
],
|
||||
)
|
||||
|
|
165
pkg/cli/client.go
Normal file
165
pkg/cli/client.go
Normal file
|
@ -0,0 +1,165 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
zotErrors "github.com/anuvu/zot/errors"
|
||||
)
|
||||
|
||||
var httpClient *http.Client = createHTTPClient() //nolint: gochecknoglobals
|
||||
|
||||
const httpTimeout = 5 * time.Second
|
||||
|
||||
func createHTTPClient() *http.Client {
|
||||
return &http.Client{
|
||||
Timeout: httpTimeout,
|
||||
}
|
||||
}
|
||||
|
||||
func makeGETRequest(url, username, password string, resultsPtr interface{}) (http.Header, error) {
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.SetBasicAuth(username, password)
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
if resp.StatusCode == http.StatusUnauthorized {
|
||||
return nil, zotErrors.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
bodyBytes, _ := ioutil.ReadAll(resp.Body)
|
||||
|
||||
return nil, errors.New(string(bodyBytes)) //nolint: goerr113
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(resp.Body).Decode(resultsPtr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp.Header, nil
|
||||
}
|
||||
|
||||
func isURL(str string) bool {
|
||||
u, err := url.Parse(str)
|
||||
return err == nil && u.Scheme != "" && u.Host != ""
|
||||
} // from https://stackoverflow.com/a/55551215
|
||||
|
||||
type requestsPool struct {
|
||||
jobs chan *manifestJob
|
||||
done chan struct{}
|
||||
waitGroup *sync.WaitGroup
|
||||
outputCh chan imageListResult
|
||||
context context.Context
|
||||
}
|
||||
|
||||
type manifestJob struct {
|
||||
url string
|
||||
username string
|
||||
password string
|
||||
outputFormat string
|
||||
imageName string
|
||||
tagName string
|
||||
manifestResp manifestResponse
|
||||
}
|
||||
|
||||
const rateLimiterBuffer = 5000
|
||||
|
||||
func newSmoothRateLimiter(ctx context.Context, wg *sync.WaitGroup, op chan imageListResult) *requestsPool {
|
||||
ch := make(chan *manifestJob, rateLimiterBuffer)
|
||||
|
||||
return &requestsPool{
|
||||
jobs: ch,
|
||||
done: make(chan struct{}),
|
||||
waitGroup: wg,
|
||||
outputCh: op,
|
||||
context: ctx,
|
||||
}
|
||||
}
|
||||
|
||||
// block every "rateLimit" time duration.
|
||||
const rateLimit = 100 * time.Millisecond
|
||||
|
||||
func (p *requestsPool) startRateLimiter() {
|
||||
p.waitGroup.Done()
|
||||
|
||||
throttle := time.NewTicker(rateLimit).C
|
||||
|
||||
for {
|
||||
select {
|
||||
case job := <-p.jobs:
|
||||
go p.doJob(job)
|
||||
case <-p.done:
|
||||
return
|
||||
}
|
||||
<-throttle
|
||||
}
|
||||
}
|
||||
|
||||
func (p *requestsPool) doJob(job *manifestJob) {
|
||||
defer p.waitGroup.Done()
|
||||
|
||||
header, err := makeGETRequest(job.url, job.username, job.password, &job.manifestResp)
|
||||
if err != nil {
|
||||
if isContextDone(p.context) {
|
||||
return
|
||||
}
|
||||
p.outputCh <- imageListResult{"", err}
|
||||
}
|
||||
|
||||
digest := header.Get("docker-content-digest")
|
||||
digest = strings.TrimPrefix(digest, "sha256:")
|
||||
|
||||
var size uint64
|
||||
|
||||
for _, layer := range job.manifestResp.Layers {
|
||||
size += layer.Size
|
||||
}
|
||||
|
||||
image := &imageStruct{}
|
||||
image.Name = job.imageName
|
||||
image.Tags = []tags{
|
||||
{
|
||||
Name: job.tagName,
|
||||
Digest: digest,
|
||||
Size: size,
|
||||
},
|
||||
}
|
||||
|
||||
str, err := image.string(job.outputFormat)
|
||||
if err != nil {
|
||||
if isContextDone(p.context) {
|
||||
return
|
||||
}
|
||||
p.outputCh <- imageListResult{"", err}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if isContextDone(p.context) {
|
||||
return
|
||||
}
|
||||
|
||||
p.outputCh <- imageListResult{str, nil}
|
||||
}
|
||||
|
||||
func (p *requestsPool) submitJob(job *manifestJob) {
|
||||
p.jobs <- job
|
||||
}
|
355
pkg/cli/config_cmd.go
Normal file
355
pkg/cli/config_cmd.go
Normal file
|
@ -0,0 +1,355 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
|
||||
zotErrors "github.com/anuvu/zot/errors"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func NewConfigCommand(configPath string) *cobra.Command {
|
||||
var isListing bool
|
||||
|
||||
var isReset bool
|
||||
|
||||
var configCmd = &cobra.Command{
|
||||
Use: "config <config-name> [variable] [value]",
|
||||
Example: examples,
|
||||
Short: "Configure zot CLI",
|
||||
Long: `Configure default parameters for CLI`,
|
||||
Args: cobra.ArbitraryArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
switch len(args) {
|
||||
case noArgs:
|
||||
if isListing { // zot config -l
|
||||
res, err := getConfigNames(configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprint(cmd.OutOrStdout(), res)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return zotErrors.ErrInvalidArgs
|
||||
case oneArg:
|
||||
// zot config <name> -l
|
||||
if isListing {
|
||||
res, err := getAllConfig(configPath, args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprint(cmd.OutOrStdout(), res)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return zotErrors.ErrInvalidArgs
|
||||
case twoArgs:
|
||||
if isReset { // zot config <name> <key> --reset
|
||||
return resetConfigValue(configPath, args[0], args[1])
|
||||
}
|
||||
// zot config <name> <key>
|
||||
res, err := getConfigValue(configPath, args[0], args[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintln(cmd.OutOrStdout(), res)
|
||||
case threeArgs:
|
||||
//zot config <name> <key> <value>
|
||||
if err := setConfigValue(configPath, args[0], args[1], args[2]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
default:
|
||||
return zotErrors.ErrInvalidArgs
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
configCmd.Flags().BoolVarP(&isListing, "list", "l", false, "List configurations")
|
||||
configCmd.Flags().BoolVar(&isReset, "reset", false, "Reset a variable value")
|
||||
configCmd.SetUsageTemplate(configCmd.UsageTemplate() + supportedOptions)
|
||||
configCmd.AddCommand(NewConfigAddCommand(configPath))
|
||||
|
||||
return configCmd
|
||||
}
|
||||
|
||||
func NewConfigAddCommand(configPath string) *cobra.Command {
|
||||
var configAddCmd = &cobra.Command{
|
||||
Use: "add <config-name> <url>",
|
||||
Short: "Add configuration for a zot URL",
|
||||
Long: `Configure CLI for interaction with a zot server`,
|
||||
Args: cobra.ExactArgs(twoArgs),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// zot config add <config-name> <url>
|
||||
err := addConfig(configPath, args[0], args[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
return configAddCmd
|
||||
}
|
||||
|
||||
func getConfigMapFromFile(filePath string) ([]interface{}, error) {
|
||||
file, err := os.OpenFile(filePath, os.O_RDONLY|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
file.Close()
|
||||
|
||||
data, err := ioutil.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var jsonMap map[string]interface{}
|
||||
|
||||
var json = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
|
||||
_ = json.Unmarshal(data, &jsonMap)
|
||||
|
||||
if jsonMap["configs"] == nil {
|
||||
return nil, ErrEmptyJSON
|
||||
}
|
||||
|
||||
return jsonMap["configs"].([]interface{}), nil
|
||||
}
|
||||
|
||||
func saveConfigMapToFile(filePath string, configMap []interface{}) error {
|
||||
var json = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
|
||||
listMap := make(map[string]interface{})
|
||||
listMap["configs"] = configMap
|
||||
marshalled, err := json.Marshal(&listMap)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile(filePath, marshalled, 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getConfigNames(configPath string) (string, error) {
|
||||
configs, err := getConfigMapFromFile(configPath)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrEmptyJSON) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return "", err
|
||||
}
|
||||
|
||||
var builder strings.Builder
|
||||
|
||||
writer := tabwriter.NewWriter(&builder, 0, 8, 1, '\t', tabwriter.AlignRight)
|
||||
|
||||
for _, val := range configs {
|
||||
configMap := val.(map[string]interface{})
|
||||
fmt.Fprintf(writer, "%s\t%s\n", configMap[nameKey], configMap["url"])
|
||||
}
|
||||
|
||||
err = writer.Flush()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return builder.String(), nil
|
||||
}
|
||||
|
||||
func addConfig(configPath, configName, url string) error {
|
||||
configs, err := getConfigMapFromFile(configPath)
|
||||
if err != nil && !errors.Is(err, ErrEmptyJSON) {
|
||||
return err
|
||||
}
|
||||
|
||||
if !isURL(url) {
|
||||
return zotErrors.ErrInvalidURL
|
||||
}
|
||||
|
||||
configMap := make(map[string]interface{})
|
||||
configMap["url"] = url
|
||||
configMap[nameKey] = configName
|
||||
configs = append(configs, configMap)
|
||||
|
||||
err = saveConfigMapToFile(configPath, configs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getConfigValue(configPath, configName, key string) (string, error) {
|
||||
configs, err := getConfigMapFromFile(configPath)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrEmptyJSON) {
|
||||
return "", zotErrors.ErrConfigNotFound
|
||||
}
|
||||
|
||||
return "", err
|
||||
}
|
||||
|
||||
for _, val := range configs {
|
||||
configMap := val.(map[string]interface{})
|
||||
|
||||
name := configMap[nameKey]
|
||||
if name == configName {
|
||||
if configMap[key] == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%v", configMap[key]), nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", zotErrors.ErrConfigNotFound
|
||||
}
|
||||
|
||||
func resetConfigValue(configPath, configName, key string) error {
|
||||
if key == "url" || key == nameKey {
|
||||
return zotErrors.ErrCannotResetConfigKey
|
||||
}
|
||||
|
||||
configs, err := getConfigMapFromFile(configPath)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrEmptyJSON) {
|
||||
return zotErrors.ErrConfigNotFound
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
for _, val := range configs {
|
||||
configMap := val.(map[string]interface{})
|
||||
|
||||
name := configMap[nameKey]
|
||||
if name == configName {
|
||||
delete(configMap, key)
|
||||
|
||||
err = saveConfigMapToFile(configPath, configs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return zotErrors.ErrConfigNotFound
|
||||
}
|
||||
|
||||
func setConfigValue(configPath, configName, key, value string) error {
|
||||
if key == nameKey {
|
||||
return zotErrors.ErrIllegalConfigKey
|
||||
}
|
||||
|
||||
configs, err := getConfigMapFromFile(configPath)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrEmptyJSON) {
|
||||
return zotErrors.ErrConfigNotFound
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
for _, val := range configs {
|
||||
configMap := val.(map[string]interface{})
|
||||
|
||||
name := configMap[nameKey]
|
||||
if name == configName {
|
||||
boolVal, err := strconv.ParseBool(value)
|
||||
if err == nil {
|
||||
configMap[key] = boolVal
|
||||
} else {
|
||||
configMap[key] = value
|
||||
}
|
||||
|
||||
err = saveConfigMapToFile(configPath, configs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return zotErrors.ErrConfigNotFound
|
||||
}
|
||||
|
||||
func getAllConfig(configPath, configName string) (string, error) {
|
||||
configs, err := getConfigMapFromFile(configPath)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrEmptyJSON) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return "", err
|
||||
}
|
||||
|
||||
var builder strings.Builder
|
||||
|
||||
for _, value := range configs {
|
||||
configMap := value.(map[string]interface{})
|
||||
|
||||
name := configMap[nameKey]
|
||||
if name == configName {
|
||||
for key, val := range configMap {
|
||||
if key == nameKey {
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Fprintf(&builder, "%s = %v\n", key, val)
|
||||
}
|
||||
|
||||
return builder.String(), nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", zotErrors.ErrConfigNotFound
|
||||
}
|
||||
|
||||
const (
|
||||
examples = ` zot config add main https://zot-foo.com:8080
|
||||
zot config main url
|
||||
zot config main --list
|
||||
zot config --list`
|
||||
|
||||
supportedOptions = `
|
||||
Useful variables:
|
||||
url zot server URL
|
||||
showspinner show spinner while loading data [true/false]`
|
||||
|
||||
nameKey = "_name"
|
||||
|
||||
noArgs = 0
|
||||
oneArg = 1
|
||||
twoArgs = 2
|
||||
threeArgs = 3
|
||||
)
|
||||
|
||||
var (
|
||||
ErrEmptyJSON = errors.New("cli: config json is empty")
|
||||
)
|
301
pkg/cli/config_cmd_test.go
Normal file
301
pkg/cli/config_cmd_test.go
Normal file
|
@ -0,0 +1,301 @@
|
|||
package cli //nolint:testpackage
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
zotErrors "github.com/anuvu/zot/errors"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestConfigCmdBasics(t *testing.T) {
|
||||
Convey("Test config help", t, func() {
|
||||
args := []string{"--help"}
|
||||
configPath := makeConfigFile("showspinner = false")
|
||||
defer os.Remove(configPath)
|
||||
cmd := NewConfigCommand(configPath)
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
cmd.SetErr(ioutil.Discard)
|
||||
cmd.SetArgs(args)
|
||||
err := cmd.Execute()
|
||||
So(buff.String(), ShouldContainSubstring, "Usage")
|
||||
So(err, ShouldBeNil)
|
||||
Convey("with the shorthand", func() {
|
||||
args[0] = "-h"
|
||||
configPath := makeConfigFile("showspinner = false")
|
||||
defer os.Remove(configPath)
|
||||
cmd := NewConfigCommand(configPath)
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
cmd.SetErr(ioutil.Discard)
|
||||
cmd.SetArgs(args)
|
||||
err := cmd.Execute()
|
||||
So(buff.String(), ShouldContainSubstring, "Usage")
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Test config no args", t, func() {
|
||||
args := []string{}
|
||||
configPath := makeConfigFile("showspinner = false")
|
||||
defer os.Remove(configPath)
|
||||
cmd := NewConfigCommand(configPath)
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
cmd.SetErr(ioutil.Discard)
|
||||
cmd.SetArgs(args)
|
||||
err := cmd.Execute()
|
||||
So(buff.String(), ShouldContainSubstring, "Usage")
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
}
|
||||
|
||||
func TestConfigCmdMain(t *testing.T) {
|
||||
Convey("Test add config", t, func() {
|
||||
args := []string{"add", "configtest1", "https://test-url.com"}
|
||||
file := makeConfigFile("")
|
||||
defer os.Remove(file)
|
||||
cmd := NewConfigCommand(file)
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
cmd.SetErr(ioutil.Discard)
|
||||
cmd.SetArgs(args)
|
||||
_ = cmd.Execute()
|
||||
|
||||
actual, err := ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
actualStr := string(actual)
|
||||
So(actualStr, ShouldContainSubstring, "configtest1")
|
||||
So(actualStr, ShouldContainSubstring, "https://test-url.com")
|
||||
})
|
||||
|
||||
Convey("Test add config with invalid URL", t, func() {
|
||||
args := []string{"add", "configtest1", "test..com"}
|
||||
file := makeConfigFile("")
|
||||
defer os.Remove(file)
|
||||
cmd := NewConfigCommand(file)
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
cmd.SetErr(ioutil.Discard)
|
||||
cmd.SetArgs(args)
|
||||
err := cmd.Execute()
|
||||
So(err, ShouldNotBeNil)
|
||||
So(err, ShouldEqual, zotErrors.ErrInvalidURL)
|
||||
})
|
||||
|
||||
Convey("Test fetch all config", t, func() {
|
||||
args := []string{"--list"}
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"configtest","url":"https://test-url.com","showspinner":false}]}`)
|
||||
defer os.Remove(configPath)
|
||||
cmd := NewConfigCommand(configPath)
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
cmd.SetErr(ioutil.Discard)
|
||||
cmd.SetArgs(args)
|
||||
err := cmd.Execute()
|
||||
So(buff.String(), ShouldContainSubstring, "https://test-url.com")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("with the shorthand", func() {
|
||||
args := []string{"-l"}
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"configtest","url":"https://test-url.com","showspinner":false}]}`)
|
||||
defer os.Remove(configPath)
|
||||
cmd := NewConfigCommand(configPath)
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
cmd.SetErr(ioutil.Discard)
|
||||
cmd.SetArgs(args)
|
||||
err := cmd.Execute()
|
||||
So(buff.String(), ShouldContainSubstring, "https://test-url.com")
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("From empty file", func() {
|
||||
args := []string{"-l"}
|
||||
configPath := makeConfigFile(``)
|
||||
defer os.Remove(configPath)
|
||||
cmd := NewConfigCommand(configPath)
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
cmd.SetErr(ioutil.Discard)
|
||||
cmd.SetArgs(args)
|
||||
err := cmd.Execute()
|
||||
So(err, ShouldBeNil)
|
||||
So(strings.TrimSpace(buff.String()), ShouldEqual, "")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Test fetch a config", t, func() {
|
||||
args := []string{"configtest", "--list"}
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"configtest","url":"https://test-url.com","showspinner":false}]}`)
|
||||
defer os.Remove(configPath)
|
||||
cmd := NewConfigCommand(configPath)
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
cmd.SetErr(ioutil.Discard)
|
||||
cmd.SetArgs(args)
|
||||
err := cmd.Execute()
|
||||
So(buff.String(), ShouldContainSubstring, "url = https://test-url.com")
|
||||
So(buff.String(), ShouldContainSubstring, "showspinner = false")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("with the shorthand", func() {
|
||||
args := []string{"configtest", "-l"}
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"configtest","url":"https://test-url.com","showspinner":false}]}`)
|
||||
defer os.Remove(configPath)
|
||||
cmd := NewConfigCommand(configPath)
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
cmd.SetErr(ioutil.Discard)
|
||||
cmd.SetArgs(args)
|
||||
err := cmd.Execute()
|
||||
So(buff.String(), ShouldContainSubstring, "url = https://test-url.com")
|
||||
So(buff.String(), ShouldContainSubstring, "showspinner = false")
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("From empty file", func() {
|
||||
args := []string{"configtest", "-l"}
|
||||
configPath := makeConfigFile(``)
|
||||
defer os.Remove(configPath)
|
||||
cmd := NewConfigCommand(configPath)
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
cmd.SetErr(ioutil.Discard)
|
||||
cmd.SetArgs(args)
|
||||
err := cmd.Execute()
|
||||
So(err, ShouldBeNil)
|
||||
So(strings.TrimSpace(buff.String()), ShouldEqual, "")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Test fetch a config val", t, func() {
|
||||
args := []string{"configtest", "url"}
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"configtest","url":"https://test-url.com","showspinner":false}]}`)
|
||||
defer os.Remove(configPath)
|
||||
cmd := NewConfigCommand(configPath)
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
cmd.SetErr(ioutil.Discard)
|
||||
cmd.SetArgs(args)
|
||||
err := cmd.Execute()
|
||||
So(buff.String(), ShouldEqual, "https://test-url.com\n")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("From empty file", func() {
|
||||
args := []string{"configtest", "url"}
|
||||
configPath := makeConfigFile(``)
|
||||
defer os.Remove(configPath)
|
||||
cmd := NewConfigCommand(configPath)
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
cmd.SetErr(buff)
|
||||
cmd.SetArgs(args)
|
||||
err := cmd.Execute()
|
||||
So(err, ShouldNotBeNil)
|
||||
So(buff.String(), ShouldContainSubstring, "does not exist")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Test add a config val", t, func() {
|
||||
args := []string{"configtest", "showspinner", "false"}
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"configtest","url":"https://test-url.com"}]}`)
|
||||
defer os.Remove(configPath)
|
||||
cmd := NewConfigCommand(configPath)
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
cmd.SetErr(ioutil.Discard)
|
||||
cmd.SetArgs(args)
|
||||
err := cmd.Execute()
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
actual, err := ioutil.ReadFile(configPath)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
actualStr := string(actual)
|
||||
So(actualStr, ShouldContainSubstring, "https://test-url.com")
|
||||
So(actualStr, ShouldContainSubstring, `"showspinner":false`)
|
||||
So(buff.String(), ShouldEqual, "")
|
||||
|
||||
Convey("To an empty file", func() {
|
||||
args := []string{"configtest", "showspinner", "false"}
|
||||
configPath := makeConfigFile(``)
|
||||
defer os.Remove(configPath)
|
||||
cmd := NewConfigCommand(configPath)
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
cmd.SetErr(ioutil.Discard)
|
||||
cmd.SetArgs(args)
|
||||
err := cmd.Execute()
|
||||
So(err, ShouldNotBeNil)
|
||||
So(buff.String(), ShouldContainSubstring, "does not exist")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Test overwrite a config", t, func() {
|
||||
args := []string{"configtest", "url", "https://new-url.com"}
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"configtest","url":"https://test-url.com","showspinner":false}]}`)
|
||||
defer os.Remove(configPath)
|
||||
cmd := NewConfigCommand(configPath)
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
cmd.SetErr(ioutil.Discard)
|
||||
cmd.SetArgs(args)
|
||||
err := cmd.Execute()
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
actual, err := ioutil.ReadFile(configPath)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
actualStr := string(actual)
|
||||
So(actualStr, ShouldContainSubstring, `https://new-url.com`)
|
||||
So(actualStr, ShouldContainSubstring, `"showspinner":false`)
|
||||
So(actualStr, ShouldNotContainSubstring, `https://test-url.com`)
|
||||
So(buff.String(), ShouldEqual, "")
|
||||
})
|
||||
|
||||
Convey("Test reset a config val", t, func() {
|
||||
args := []string{"configtest", "showspinner", "--reset"}
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"configtest","url":"https://test-url.com","showspinner":false}]}`)
|
||||
defer os.Remove(configPath)
|
||||
cmd := NewConfigCommand(configPath)
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
cmd.SetErr(ioutil.Discard)
|
||||
cmd.SetArgs(args)
|
||||
err := cmd.Execute()
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
actual, err := ioutil.ReadFile(configPath)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
actualStr := string(actual)
|
||||
So(actualStr, ShouldNotContainSubstring, "showspinner")
|
||||
So(actualStr, ShouldContainSubstring, `"url":"https://test-url.com"`)
|
||||
So(buff.String(), ShouldEqual, "")
|
||||
})
|
||||
|
||||
Convey("Test reset a url", t, func() {
|
||||
args := []string{"configtest", "url", "--reset"}
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"configtest","url":"https://test-url.com","showspinner":false}]}`)
|
||||
defer os.Remove(configPath)
|
||||
cmd := NewConfigCommand(configPath)
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
cmd.SetErr(ioutil.Discard)
|
||||
cmd.SetArgs(args)
|
||||
err := cmd.Execute()
|
||||
So(err, ShouldNotBeNil)
|
||||
So(buff.String(), ShouldContainSubstring, "cannot reset")
|
||||
})
|
||||
}
|
123
pkg/cli/image_cmd.go
Normal file
123
pkg/cli/image_cmd.go
Normal file
|
@ -0,0 +1,123 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
zotErrors "github.com/anuvu/zot/errors"
|
||||
"github.com/briandowns/spinner"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func NewImageCommand(searchService ImageSearchService, configPath string) *cobra.Command {
|
||||
searchImageParams := make(map[string]*string)
|
||||
|
||||
var servURL string
|
||||
|
||||
var user string
|
||||
|
||||
var outputFormat string
|
||||
|
||||
var imageCmd = &cobra.Command{
|
||||
Use: "images [config-name]",
|
||||
Short: "List hosted images",
|
||||
Long: `List images hosted on zot`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if servURL == "" {
|
||||
if len(args) > 0 {
|
||||
urlFromConfig, err := getConfigValue(configPath, args[0], "url")
|
||||
if err != nil {
|
||||
cmd.SilenceUsage = true
|
||||
return err
|
||||
}
|
||||
if urlFromConfig == "" {
|
||||
return zotErrors.ErrNoURLProvided
|
||||
}
|
||||
servURL = urlFromConfig
|
||||
} else {
|
||||
return zotErrors.ErrNoURLProvided
|
||||
}
|
||||
}
|
||||
|
||||
var isSpinner bool
|
||||
|
||||
if len(args) > 0 {
|
||||
var err error
|
||||
isSpinner, err = isSpinnerEnabled(configPath, args[0])
|
||||
if err != nil {
|
||||
cmd.SilenceUsage = true
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
isSpinner = true
|
||||
}
|
||||
|
||||
err := searchImage(cmd, searchImageParams, searchService, &servURL, &user, &outputFormat, isSpinner)
|
||||
|
||||
if err != nil {
|
||||
cmd.SilenceUsage = true
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
setupCmdFlags(imageCmd, searchImageParams, &servURL, &user, &outputFormat)
|
||||
imageCmd.SetUsageTemplate(imageCmd.UsageTemplate() + usageFooter)
|
||||
|
||||
return imageCmd
|
||||
}
|
||||
|
||||
func isSpinnerEnabled(configPath, configName string) (bool, error) {
|
||||
spinnerConfig, err := getConfigValue(configPath, configName, "showspinner")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if spinnerConfig == "" {
|
||||
return true, nil // spinner is enabled by default
|
||||
}
|
||||
|
||||
isSpinner, err := strconv.ParseBool(spinnerConfig)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return isSpinner, nil
|
||||
}
|
||||
|
||||
func setupCmdFlags(imageCmd *cobra.Command, searchImageParams map[string]*string, servURL, user, outputFormat *string) {
|
||||
searchImageParams["imageName"] = imageCmd.Flags().StringP("name", "n", "", "List image details by name")
|
||||
|
||||
imageCmd.Flags().StringVar(servURL, "url", "", "Specify zot server URL if config-name is not mentioned")
|
||||
imageCmd.Flags().StringVarP(user, "user", "u", "", `User Credentials of zot server in "username:password" format`)
|
||||
imageCmd.Flags().StringVarP(outputFormat, "output", "o", "", "Specify output format [text/json/yaml]")
|
||||
}
|
||||
|
||||
func searchImage(cmd *cobra.Command, params map[string]*string,
|
||||
service ImageSearchService, servURL, user, outputFormat *string, isSpinner bool) error {
|
||||
spin := spinner.New(spinner.CharSets[39], spinnerDuration, spinner.WithWriter(cmd.ErrOrStderr()))
|
||||
spin.Prefix = "Searching... "
|
||||
|
||||
for _, searcher := range getSearchers() {
|
||||
found, err := searcher.search(params, service, servURL, user, outputFormat,
|
||||
cmd.OutOrStdout(), spinnerState{spin, isSpinner})
|
||||
if found {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return zotErrors.ErrInvalidFlagsCombination
|
||||
}
|
||||
|
||||
const (
|
||||
spinnerDuration = 150 * time.Millisecond
|
||||
usageFooter = `
|
||||
Run 'zot config -h' for details on [config-name] argument
|
||||
`
|
||||
)
|
499
pkg/cli/image_cmd_test.go
Normal file
499
pkg/cli/image_cmd_test.go
Normal file
|
@ -0,0 +1,499 @@
|
|||
package cli //nolint:testpackage
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"gopkg.in/resty.v1"
|
||||
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
zotErrors "github.com/anuvu/zot/errors"
|
||||
"github.com/anuvu/zot/pkg/api"
|
||||
"github.com/anuvu/zot/pkg/compliance/v1_0_0"
|
||||
godigest "github.com/opencontainers/go-digest"
|
||||
ispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestSearchImageCmd(t *testing.T) {
|
||||
Convey("Test image help", t, func() {
|
||||
args := []string{"--help"}
|
||||
configPath := makeConfigFile("")
|
||||
defer os.Remove(configPath)
|
||||
cmd := NewImageCommand(new(mockService), configPath)
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
cmd.SetErr(ioutil.Discard)
|
||||
cmd.SetArgs(args)
|
||||
err := cmd.Execute()
|
||||
So(buff.String(), ShouldContainSubstring, "Usage")
|
||||
So(err, ShouldBeNil)
|
||||
Convey("with the shorthand", func() {
|
||||
args[0] = "-h"
|
||||
configPath := makeConfigFile("")
|
||||
defer os.Remove(configPath)
|
||||
cmd := NewImageCommand(new(mockService), configPath)
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
cmd.SetErr(ioutil.Discard)
|
||||
cmd.SetArgs(args)
|
||||
err := cmd.Execute()
|
||||
So(buff.String(), ShouldContainSubstring, "Usage")
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
})
|
||||
Convey("Test image no url", t, func() {
|
||||
args := []string{"imagetest", "--name", "dummyIdRandom"}
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","showspinner":false}]}`)
|
||||
defer os.Remove(configPath)
|
||||
cmd := NewImageCommand(new(mockService), configPath)
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
cmd.SetErr(ioutil.Discard)
|
||||
cmd.SetArgs(args)
|
||||
err := cmd.Execute()
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("Test image no params", t, func() {
|
||||
args := []string{"imagetest", "--url", "someUrl"}
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","showspinner":false}]}`)
|
||||
defer os.Remove(configPath)
|
||||
cmd := NewImageCommand(new(mockService), configPath)
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
cmd.SetErr(ioutil.Discard)
|
||||
cmd.SetArgs(args)
|
||||
err := cmd.Execute()
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("Test image invalid url", t, func() {
|
||||
args := []string{"imagetest", "--name", "dummyImageName", "--url", "invalidUrl"}
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","showspinner":false}]}`)
|
||||
defer os.Remove(configPath)
|
||||
cmd := NewImageCommand(new(searchService), configPath)
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
cmd.SetErr(buff)
|
||||
cmd.SetArgs(args)
|
||||
err := cmd.Execute()
|
||||
So(err, ShouldNotBeNil)
|
||||
So(err, ShouldEqual, zotErrors.ErrInvalidURL)
|
||||
So(buff.String(), ShouldContainSubstring, "invalid URL format")
|
||||
})
|
||||
Convey("Test image invalid url port", t, func() {
|
||||
args := []string{"imagetest", "--name", "dummyImageName", "--url", "http://localhost:99999"}
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","showspinner":false}]}`)
|
||||
defer os.Remove(configPath)
|
||||
cmd := NewImageCommand(new(searchService), configPath)
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
cmd.SetErr(buff)
|
||||
cmd.SetArgs(args)
|
||||
err := cmd.Execute()
|
||||
So(err, ShouldNotBeNil)
|
||||
So(buff.String(), ShouldContainSubstring, "invalid port")
|
||||
|
||||
Convey("without flags", func() {
|
||||
args := []string{"imagetest", "--url", "http://localhost:99999"}
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","showspinner":false}]}`)
|
||||
defer os.Remove(configPath)
|
||||
cmd := NewImageCommand(new(searchService), configPath)
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
cmd.SetErr(buff)
|
||||
cmd.SetArgs(args)
|
||||
err = cmd.Execute()
|
||||
So(err, ShouldNotBeNil)
|
||||
So(buff.String(), ShouldContainSubstring, "invalid port")
|
||||
})
|
||||
})
|
||||
Convey("Test image unreachable", t, func() {
|
||||
args := []string{"imagetest", "--name", "dummyImageName", "--url", "http://localhost:9999"}
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","showspinner":false}]}`)
|
||||
defer os.Remove(configPath)
|
||||
cmd := NewImageCommand(new(searchService), configPath)
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
cmd.SetErr(buff)
|
||||
cmd.SetArgs(args)
|
||||
err := cmd.Execute()
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("Test image url from config", t, func() {
|
||||
args := []string{"imagetest", "--name", "dummyImageName"}
|
||||
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","url":"https://test-url.com","showspinner":false}]}`)
|
||||
defer os.Remove(configPath)
|
||||
|
||||
cmd := NewImageCommand(new(mockService), configPath)
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
cmd.SetErr(ioutil.Discard)
|
||||
cmd.SetArgs(args)
|
||||
err := cmd.Execute()
|
||||
space := regexp.MustCompile(`\s+`)
|
||||
str := space.ReplaceAllString(buff.String(), " ")
|
||||
So(strings.TrimSpace(str), ShouldEqual, "IMAGE NAME TAG DIGEST SIZE dummyImageName tag DigestsA 123kB")
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("Test image by name", t, func() {
|
||||
args := []string{"imagetest", "--name", "dummyImageName", "--url", "someUrlImage"}
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","showspinner":false}]}`)
|
||||
defer os.Remove(configPath)
|
||||
imageCmd := NewImageCommand(new(mockService), configPath)
|
||||
buff := bytes.NewBufferString("")
|
||||
imageCmd.SetOut(buff)
|
||||
imageCmd.SetErr(ioutil.Discard)
|
||||
imageCmd.SetArgs(args)
|
||||
err := imageCmd.Execute()
|
||||
space := regexp.MustCompile(`\s+`)
|
||||
str := space.ReplaceAllString(buff.String(), " ")
|
||||
So(strings.TrimSpace(str), ShouldEqual, "IMAGE NAME TAG DIGEST SIZE dummyImageName tag DigestsA 123kB")
|
||||
So(err, ShouldBeNil)
|
||||
Convey("using shorthand", func() {
|
||||
args := []string{"imagetest", "-n", "dummyImageName", "--url", "someUrlImage"}
|
||||
buff := bytes.NewBufferString("")
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","showspinner":false}]}`)
|
||||
defer os.Remove(configPath)
|
||||
imageCmd := NewImageCommand(new(mockService), configPath)
|
||||
imageCmd.SetOut(buff)
|
||||
imageCmd.SetErr(ioutil.Discard)
|
||||
imageCmd.SetArgs(args)
|
||||
err := imageCmd.Execute()
|
||||
|
||||
space := regexp.MustCompile(`\s+`)
|
||||
str := space.ReplaceAllString(buff.String(), " ")
|
||||
So(strings.TrimSpace(str), ShouldEqual, "IMAGE NAME TAG DIGEST SIZE dummyImageName tag DigestsA 123kB")
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOutputFormat(t *testing.T) {
|
||||
Convey("Test text", t, func() {
|
||||
args := []string{"imagetest", "--name", "dummyImageName", "-o", "text"}
|
||||
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","url":"https://test-url.com","showspinner":false}]}`)
|
||||
defer os.Remove(configPath)
|
||||
|
||||
cmd := NewImageCommand(new(mockService), configPath)
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
cmd.SetErr(ioutil.Discard)
|
||||
cmd.SetArgs(args)
|
||||
err := cmd.Execute()
|
||||
space := regexp.MustCompile(`\s+`)
|
||||
str := space.ReplaceAllString(buff.String(), " ")
|
||||
So(strings.TrimSpace(str), ShouldEqual, "IMAGE NAME TAG DIGEST SIZE dummyImageName tag DigestsA 123kB")
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("Test json", t, func() {
|
||||
args := []string{"imagetest", "--name", "dummyImageName", "-o", "json"}
|
||||
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","url":"https://test-url.com","showspinner":false}]}`)
|
||||
defer os.Remove(configPath)
|
||||
|
||||
cmd := NewImageCommand(new(mockService), configPath)
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
cmd.SetErr(ioutil.Discard)
|
||||
cmd.SetArgs(args)
|
||||
err := cmd.Execute()
|
||||
space := regexp.MustCompile(`\s+`)
|
||||
str := space.ReplaceAllString(buff.String(), " ")
|
||||
So(strings.TrimSpace(str), ShouldEqual, `{ "name": "dummyImageName", "tags": [ { "name":`+
|
||||
` "tag", "size": 123445, "digest": "DigestsAreReallyLong" } ] }`)
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("Test yaml", t, func() {
|
||||
args := []string{"imagetest", "--name", "dummyImageName", "-o", "yaml"}
|
||||
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","url":"https://test-url.com","showspinner":false}]}`)
|
||||
defer os.Remove(configPath)
|
||||
|
||||
cmd := NewImageCommand(new(mockService), configPath)
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
cmd.SetErr(ioutil.Discard)
|
||||
cmd.SetArgs(args)
|
||||
err := cmd.Execute()
|
||||
space := regexp.MustCompile(`\s+`)
|
||||
str := space.ReplaceAllString(buff.String(), " ")
|
||||
So(strings.TrimSpace(str), ShouldEqual, `name: dummyImageName tags: -`+
|
||||
` name: tag size: 123445 digest: DigestsAreReallyLong`)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("Test yml", func() {
|
||||
args := []string{"imagetest", "--name", "dummyImageName", "-o", "yml"}
|
||||
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","url":"https://test-url.com","showspinner":false}]}`)
|
||||
defer os.Remove(configPath)
|
||||
|
||||
cmd := NewImageCommand(new(mockService), configPath)
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
cmd.SetErr(ioutil.Discard)
|
||||
cmd.SetArgs(args)
|
||||
err := cmd.Execute()
|
||||
space := regexp.MustCompile(`\s+`)
|
||||
str := space.ReplaceAllString(buff.String(), " ")
|
||||
So(strings.TrimSpace(str), ShouldEqual, "name: dummyImageName tags: - name: "+
|
||||
"tag size: 123445 digest: DigestsAreReallyLong")
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Test invalid", t, func() {
|
||||
args := []string{"imagetest", "--name", "dummyImageName", "-o", "random"}
|
||||
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","url":"https://test-url.com","showspinner":false}]}`)
|
||||
defer os.Remove(configPath)
|
||||
|
||||
cmd := NewImageCommand(new(mockService), configPath)
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
cmd.SetErr(buff)
|
||||
cmd.SetArgs(args)
|
||||
err := cmd.Execute()
|
||||
So(err, ShouldNotBeNil)
|
||||
So(buff.String(), ShouldContainSubstring, "invalid output format")
|
||||
})
|
||||
}
|
||||
|
||||
func TestServerResponse(t *testing.T) {
|
||||
Convey("Test from real server", t, func() {
|
||||
port := "8080"
|
||||
url := "http://127.0.0.1:8080"
|
||||
config := api.NewConfig()
|
||||
config.HTTP.Port = port
|
||||
c := api.NewController(config)
|
||||
dir, err := ioutil.TempDir("", "oci-repo-test")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
c.Config.Storage.RootDirectory = dir
|
||||
go func(controller *api.Controller) {
|
||||
// this blocks
|
||||
if err := controller.Run(); err != nil {
|
||||
return
|
||||
}
|
||||
}(c)
|
||||
// wait till ready
|
||||
for {
|
||||
_, err := resty.R().Get(url)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
defer func(controller *api.Controller) {
|
||||
ctx := context.Background()
|
||||
_ = controller.Server.Shutdown(ctx)
|
||||
}(c)
|
||||
|
||||
uploadManifest(url)
|
||||
|
||||
Convey("Test all images config url", func() {
|
||||
args := []string{"imagetest"}
|
||||
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url))
|
||||
defer os.Remove(configPath)
|
||||
cmd := NewImageCommand(new(searchService), configPath)
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
cmd.SetErr(buff)
|
||||
cmd.SetArgs(args)
|
||||
err = cmd.Execute()
|
||||
So(err, ShouldBeNil)
|
||||
space := regexp.MustCompile(`\s+`)
|
||||
str := space.ReplaceAllString(buff.String(), " ")
|
||||
actual := strings.TrimSpace(str)
|
||||
So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST SIZE")
|
||||
So(actual, ShouldContainSubstring, "repo7 test:2.0 a0ca253b 15B")
|
||||
So(actual, ShouldContainSubstring, "repo7 test:1.0 a0ca253b 15B")
|
||||
})
|
||||
|
||||
Convey("Test image by name config url", func() {
|
||||
args := []string{"imagetest", "--name", "repo7"}
|
||||
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url))
|
||||
defer os.Remove(configPath)
|
||||
cmd := NewImageCommand(new(searchService), configPath)
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
cmd.SetErr(buff)
|
||||
cmd.SetArgs(args)
|
||||
err = cmd.Execute()
|
||||
So(err, ShouldBeNil)
|
||||
space := regexp.MustCompile(`\s+`)
|
||||
str := space.ReplaceAllString(buff.String(), " ")
|
||||
actual := strings.TrimSpace(str)
|
||||
So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST SIZE")
|
||||
So(actual, ShouldContainSubstring, "repo7 test:2.0 a0ca253b 15B")
|
||||
So(actual, ShouldContainSubstring, "repo7 test:1.0 a0ca253b 15B")
|
||||
|
||||
Convey("with shorthand", func() {
|
||||
args := []string{"imagetest", "-n", "repo7"}
|
||||
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url))
|
||||
defer os.Remove(configPath)
|
||||
cmd := NewImageCommand(new(searchService), configPath)
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
cmd.SetErr(buff)
|
||||
cmd.SetArgs(args)
|
||||
err = cmd.Execute()
|
||||
So(err, ShouldBeNil)
|
||||
space := regexp.MustCompile(`\s+`)
|
||||
str := space.ReplaceAllString(buff.String(), " ")
|
||||
actual := strings.TrimSpace(str)
|
||||
So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST SIZE")
|
||||
So(actual, ShouldContainSubstring, "repo7 test:2.0 a0ca253b 15B")
|
||||
So(actual, ShouldContainSubstring, "repo7 test:1.0 a0ca253b 15B")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Test image by name invalid name", func() {
|
||||
args := []string{"imagetest", "--name", "repo777"}
|
||||
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url))
|
||||
defer os.Remove(configPath)
|
||||
cmd := NewImageCommand(new(searchService), configPath)
|
||||
buff := bytes.NewBufferString("")
|
||||
cmd.SetOut(buff)
|
||||
cmd.SetErr(buff)
|
||||
cmd.SetArgs(args)
|
||||
err = cmd.Execute()
|
||||
So(err, ShouldNotBeNil)
|
||||
actual := buff.String()
|
||||
So(actual, ShouldContainSubstring, "unknown")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func uploadManifest(url string) {
|
||||
// create a blob/layer
|
||||
resp, _ := resty.R().Post(url + "/v2/repo7/blobs/uploads/")
|
||||
loc := v1_0_0.Location(url, resp)
|
||||
|
||||
content := []byte("this is a blob5")
|
||||
digest := godigest.FromBytes(content)
|
||||
_, _ = resty.R().SetQueryParam("digest", digest.String()).
|
||||
SetHeader("Content-Type", "application/octet-stream").SetBody(content).Put(loc)
|
||||
|
||||
// create a manifest
|
||||
m := ispec.Manifest{
|
||||
Config: ispec.Descriptor{
|
||||
Digest: digest,
|
||||
Size: int64(len(content)),
|
||||
},
|
||||
Layers: []ispec.Descriptor{
|
||||
{
|
||||
MediaType: "application/vnd.oci.image.layer.v1.tar",
|
||||
Digest: digest,
|
||||
Size: int64(len(content)),
|
||||
},
|
||||
},
|
||||
}
|
||||
m.SchemaVersion = 2
|
||||
content, _ = json.Marshal(m)
|
||||
_, _ = resty.R().SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json").
|
||||
SetBody(content).Put(url + "/v2/repo7/manifests/test:1.0")
|
||||
|
||||
content = []byte("this is a blob5")
|
||||
digest = godigest.FromBytes(content)
|
||||
// create a manifest with same blob but a different tag
|
||||
m = ispec.Manifest{
|
||||
Config: ispec.Descriptor{
|
||||
Digest: digest,
|
||||
Size: int64(len(content)),
|
||||
},
|
||||
Layers: []ispec.Descriptor{
|
||||
{
|
||||
MediaType: "application/vnd.oci.image.layer.v1.tar",
|
||||
Digest: digest,
|
||||
Size: int64(len(content)),
|
||||
},
|
||||
},
|
||||
}
|
||||
m.SchemaVersion = 2
|
||||
content, _ = json.Marshal(m)
|
||||
_, _ = resty.R().SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json").
|
||||
SetBody(content).Put(url + "/v2/repo7/manifests/test:2.0")
|
||||
}
|
||||
|
||||
type mockService struct{}
|
||||
|
||||
func (service mockService) getAllImages(ctx context.Context, serverURL, username, password,
|
||||
outputFormat string, channel chan imageListResult, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
|
||||
image := &imageStruct{}
|
||||
image.Name = "randomimageName"
|
||||
image.Tags = []tags{
|
||||
{
|
||||
Name: "tag",
|
||||
Digest: "DigestsAreReallyLong",
|
||||
Size: 123445,
|
||||
},
|
||||
}
|
||||
|
||||
str, err := image.string(outputFormat)
|
||||
if err != nil {
|
||||
channel <- imageListResult{"", err}
|
||||
return
|
||||
}
|
||||
channel <- imageListResult{str, nil}
|
||||
}
|
||||
|
||||
func (service mockService) getImageByName(ctx context.Context, serverURL, username, password,
|
||||
imageName, outputFormat string, channel chan imageListResult, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
|
||||
image := &imageStruct{}
|
||||
image.Name = imageName
|
||||
image.Tags = []tags{
|
||||
{
|
||||
Name: "tag",
|
||||
Digest: "DigestsAreReallyLong",
|
||||
Size: 123445,
|
||||
},
|
||||
}
|
||||
|
||||
str, err := image.string(outputFormat)
|
||||
if err != nil {
|
||||
channel <- imageListResult{"", err}
|
||||
return
|
||||
}
|
||||
channel <- imageListResult{str, nil}
|
||||
}
|
||||
|
||||
func makeConfigFile(content string) string {
|
||||
f, err := ioutil.TempFile("", "config-*.properties")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
|
||||
text := []byte(content)
|
||||
if err := ioutil.WriteFile(f.Name(), text, 0600); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return f.Name()
|
||||
}
|
|
@ -19,7 +19,7 @@ func metadataConfig(md *mapstructure.Metadata) viper.DecoderConfigOption {
|
|||
}
|
||||
}
|
||||
|
||||
func NewRootCmd() *cobra.Command {
|
||||
func NewRootCmd(configPath string) *cobra.Command {
|
||||
showVersion := false
|
||||
config := api.NewConfig()
|
||||
|
||||
|
@ -96,6 +96,10 @@ func NewRootCmd() *cobra.Command {
|
|||
|
||||
rootCmd.AddCommand(serveCmd)
|
||||
rootCmd.AddCommand(gcCmd)
|
||||
|
||||
rootCmd.AddCommand(NewConfigCommand(configPath))
|
||||
rootCmd.AddCommand(NewImageCommand(NewImageSearchService(), configPath))
|
||||
|
||||
rootCmd.Flags().BoolVarP(&showVersion, "version", "v", false, "show the version and exit")
|
||||
|
||||
return rootCmd
|
||||
|
|
|
@ -3,6 +3,7 @@ package cli_test
|
|||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/anuvu/zot/pkg/cli"
|
||||
|
@ -16,13 +17,13 @@ func TestUsage(t *testing.T) {
|
|||
|
||||
Convey("Test usage", t, func(c C) {
|
||||
os.Args = []string{"cli_test", "help"}
|
||||
err := cli.NewRootCmd().Execute()
|
||||
err := cli.NewRootCmd(os.TempDir()).Execute()
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("Test version", t, func(c C) {
|
||||
os.Args = []string{"cli_test", "--version"}
|
||||
err := cli.NewRootCmd().Execute()
|
||||
err := cli.NewRootCmd(os.TempDir()).Execute()
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
}
|
||||
|
@ -34,19 +35,19 @@ func TestServe(t *testing.T) {
|
|||
|
||||
Convey("Test serve help", t, func(c C) {
|
||||
os.Args = []string{"cli_test", "serve", "-h"}
|
||||
err := cli.NewRootCmd().Execute()
|
||||
err := cli.NewRootCmd(os.TempDir()).Execute()
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("Test serve config", t, func(c C) {
|
||||
Convey("unknown config", func(c C) {
|
||||
os.Args = []string{"cli_test", "serve", "/tmp/x"}
|
||||
So(func() { _ = cli.NewRootCmd().Execute() }, ShouldPanic)
|
||||
os.Args = []string{"cli_test", "serve", path.Join(os.TempDir(), "/x")}
|
||||
So(func() { _ = cli.NewRootCmd(os.TempDir()).Execute() }, ShouldPanic)
|
||||
})
|
||||
|
||||
Convey("non-existent config", func(c C) {
|
||||
os.Args = []string{"cli_test", "serve", "/tmp/x.yaml"}
|
||||
So(func() { _ = cli.NewRootCmd().Execute() }, ShouldPanic)
|
||||
os.Args = []string{"cli_test", "serve", path.Join(os.TempDir(), "/x.yaml")}
|
||||
So(func() { _ = cli.NewRootCmd(os.TempDir()).Execute() }, ShouldPanic)
|
||||
})
|
||||
|
||||
Convey("bad config", func(c C) {
|
||||
|
@ -59,7 +60,7 @@ func TestServe(t *testing.T) {
|
|||
err = tmpfile.Close()
|
||||
So(err, ShouldBeNil)
|
||||
os.Args = []string{"cli_test", "serve", tmpfile.Name()}
|
||||
So(func() { _ = cli.NewRootCmd().Execute() }, ShouldPanic)
|
||||
So(func() { _ = cli.NewRootCmd(os.TempDir()).Execute() }, ShouldPanic)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -71,7 +72,7 @@ func TestGC(t *testing.T) {
|
|||
|
||||
Convey("Test gc", t, func(c C) {
|
||||
os.Args = []string{"cli_test", "garbage-collect", "-h"}
|
||||
err := cli.NewRootCmd().Execute()
|
||||
err := cli.NewRootCmd(os.TempDir()).Execute()
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
}
|
||||
|
|
215
pkg/cli/searcher.go
Normal file
215
pkg/cli/searcher.go
Normal file
|
@ -0,0 +1,215 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/briandowns/spinner"
|
||||
)
|
||||
|
||||
func getSearchers() []searcher {
|
||||
searchers := []searcher{
|
||||
new(allImagesSearcher),
|
||||
new(imageByNameSearcher),
|
||||
}
|
||||
|
||||
return searchers
|
||||
}
|
||||
|
||||
type searcher interface {
|
||||
search(params map[string]*string, searchService ImageSearchService,
|
||||
servURL, user, outputFormat *string, stdWriter io.Writer, spinner spinnerState) (bool, error)
|
||||
}
|
||||
|
||||
func canSearch(params map[string]*string, requiredParams *set) bool {
|
||||
for key, value := range params {
|
||||
if requiredParams.contains(key) && *value == "" {
|
||||
return false
|
||||
} else if !requiredParams.contains(key) && *value != "" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
type allImagesSearcher struct{}
|
||||
|
||||
func (search allImagesSearcher) search(params map[string]*string, searchService ImageSearchService,
|
||||
servURL, user, outputFormat *string, stdWriter io.Writer, spinner spinnerState) (bool, error) {
|
||||
if !canSearch(params, newSet("")) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
username, password := getUsernameAndPassword(*user)
|
||||
imageErr := make(chan imageListResult)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
wg.Add(1)
|
||||
|
||||
go searchService.getAllImages(ctx, *servURL, username, password, *outputFormat, imageErr, &wg)
|
||||
wg.Add(1)
|
||||
|
||||
var errCh chan error = make(chan error, 1)
|
||||
|
||||
go collectImages(outputFormat, stdWriter, &wg, imageErr, cancel, spinner, errCh)
|
||||
wg.Wait()
|
||||
select {
|
||||
case err := <-errCh:
|
||||
return true, err
|
||||
default:
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
type imageByNameSearcher struct{}
|
||||
|
||||
func (search imageByNameSearcher) search(params map[string]*string,
|
||||
searchService ImageSearchService, servURL, user, outputFormat *string,
|
||||
stdWriter io.Writer, spinner spinnerState) (bool, error) {
|
||||
if !canSearch(params, newSet("imageName")) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
username, password := getUsernameAndPassword(*user)
|
||||
imageErr := make(chan imageListResult)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
wg.Add(1)
|
||||
|
||||
go searchService.getImageByName(ctx, *servURL, username, password, *params["imageName"], *outputFormat, imageErr, &wg)
|
||||
wg.Add(1)
|
||||
|
||||
var errCh chan error = make(chan error, 1)
|
||||
go collectImages(outputFormat, stdWriter, &wg, imageErr, cancel, spinner, errCh)
|
||||
|
||||
wg.Wait()
|
||||
|
||||
select {
|
||||
case err := <-errCh:
|
||||
return true, err
|
||||
default:
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
func collectImages(outputFormat *string, stdWriter io.Writer, wg *sync.WaitGroup,
|
||||
imageErr chan imageListResult, cancel context.CancelFunc, spinner spinnerState, errCh chan error) {
|
||||
var foundResult bool
|
||||
|
||||
defer wg.Done()
|
||||
spinner.startSpinner()
|
||||
|
||||
for {
|
||||
select {
|
||||
case result := <-imageErr:
|
||||
if result.Err != nil {
|
||||
spinner.stopSpinner()
|
||||
cancel()
|
||||
errCh <- result.Err
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if !foundResult && (*outputFormat == "text" || *outputFormat == "") {
|
||||
spinner.stopSpinner()
|
||||
|
||||
var builder strings.Builder
|
||||
|
||||
printImageTableHeader(&builder)
|
||||
fmt.Fprint(stdWriter, builder.String())
|
||||
}
|
||||
|
||||
foundResult = true
|
||||
|
||||
fmt.Fprint(stdWriter, result.StrValue)
|
||||
case <-time.After(waitTimeout):
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getUsernameAndPassword(user string) (string, string) {
|
||||
if strings.Contains(user, ":") {
|
||||
split := strings.Split(user, ":")
|
||||
return split[0], split[1]
|
||||
}
|
||||
|
||||
return "", ""
|
||||
}
|
||||
|
||||
type set struct {
|
||||
m map[string]struct{}
|
||||
}
|
||||
|
||||
func getEmptyStruct() struct{} {
|
||||
return struct{}{}
|
||||
}
|
||||
|
||||
func newSet(initialValues ...string) *set {
|
||||
s := &set{}
|
||||
s.m = make(map[string]struct{})
|
||||
|
||||
for _, val := range initialValues {
|
||||
s.m[val] = getEmptyStruct()
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *set) contains(value string) bool {
|
||||
_, c := s.m[value]
|
||||
return c
|
||||
}
|
||||
|
||||
var (
|
||||
ErrCannotSearch = errors.New("cannot search with these parameters")
|
||||
ErrInvalidOutputFormat = errors.New("invalid output format")
|
||||
)
|
||||
|
||||
type imageListResult struct {
|
||||
StrValue string
|
||||
Err error
|
||||
}
|
||||
|
||||
type spinnerState struct {
|
||||
spinner *spinner.Spinner
|
||||
enabled bool
|
||||
}
|
||||
|
||||
func (spinner *spinnerState) startSpinner() {
|
||||
if spinner.enabled {
|
||||
spinner.spinner.Start()
|
||||
}
|
||||
}
|
||||
|
||||
func (spinner *spinnerState) stopSpinner() {
|
||||
if spinner.enabled && spinner.spinner.Active() {
|
||||
spinner.spinner.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
func printImageTableHeader(writer io.Writer) {
|
||||
table := getNoBorderTableWriter(writer)
|
||||
row := []string{"IMAGE NAME",
|
||||
"TAG",
|
||||
"DIGEST",
|
||||
"SIZE",
|
||||
}
|
||||
table.Append(row)
|
||||
table.Render()
|
||||
}
|
||||
|
||||
const (
|
||||
waitTimeout = 2 * time.Second
|
||||
)
|
307
pkg/cli/service.go
Normal file
307
pkg/cli/service.go
Normal file
|
@ -0,0 +1,307 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"gopkg.in/yaml.v2"
|
||||
|
||||
zotErrors "github.com/anuvu/zot/errors"
|
||||
)
|
||||
|
||||
type ImageSearchService interface {
|
||||
getAllImages(ctx context.Context, serverURL, username, password,
|
||||
outputFormat string, channel chan imageListResult, wg *sync.WaitGroup)
|
||||
getImageByName(ctx context.Context, serverURL, username, password, imageName, outputFormat string,
|
||||
channel chan imageListResult, wg *sync.WaitGroup)
|
||||
}
|
||||
type searchService struct{}
|
||||
|
||||
func NewImageSearchService() ImageSearchService {
|
||||
return searchService{}
|
||||
}
|
||||
|
||||
func (service searchService) getImageByName(ctx context.Context, url, username, password,
|
||||
imageName, outputFormat string, c chan imageListResult, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
|
||||
p := newSmoothRateLimiter(ctx, wg, c)
|
||||
|
||||
wg.Add(1)
|
||||
|
||||
go p.startRateLimiter()
|
||||
wg.Add(1)
|
||||
|
||||
go getImage(ctx, url, username, password, imageName, outputFormat, c, wg, p)
|
||||
}
|
||||
|
||||
func (service searchService) getAllImages(ctx context.Context, url, username, password,
|
||||
outputFormat string, c chan imageListResult, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
|
||||
catalog := &catalogResponse{}
|
||||
|
||||
catalogEndPoint, err := combineServerAndEndpointURL(url, "/v2/_catalog")
|
||||
if err != nil {
|
||||
if isContextDone(ctx) {
|
||||
return
|
||||
}
|
||||
c <- imageListResult{"", err}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
_, err = makeGETRequest(catalogEndPoint, username, password, catalog)
|
||||
if err != nil {
|
||||
if isContextDone(ctx) {
|
||||
return
|
||||
}
|
||||
c <- imageListResult{"", err}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
p := newSmoothRateLimiter(ctx, wg, c)
|
||||
|
||||
wg.Add(1)
|
||||
|
||||
go p.startRateLimiter()
|
||||
|
||||
for _, repo := range catalog.Repositories {
|
||||
wg.Add(1)
|
||||
|
||||
go getImage(ctx, url, username, password, repo, outputFormat, c, wg, p)
|
||||
}
|
||||
}
|
||||
func getImage(ctx context.Context, url, username, password, imageName, outputFormat string,
|
||||
c chan imageListResult, wg *sync.WaitGroup, pool *requestsPool) {
|
||||
defer wg.Done()
|
||||
|
||||
tagListEndpoint, err := combineServerAndEndpointURL(url, fmt.Sprintf("/v2/%s/tags/list", imageName))
|
||||
if err != nil {
|
||||
if isContextDone(ctx) {
|
||||
return
|
||||
}
|
||||
c <- imageListResult{"", err}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
tagsList := &tagListResp{}
|
||||
_, err = makeGETRequest(tagListEndpoint, username, password, &tagsList)
|
||||
|
||||
if err != nil {
|
||||
if isContextDone(ctx) {
|
||||
return
|
||||
}
|
||||
c <- imageListResult{"", err}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
for _, tag := range tagsList.Tags {
|
||||
wg.Add(1)
|
||||
|
||||
go addManifestCallToPool(ctx, pool, url, username, password, imageName, tag, outputFormat, c, wg)
|
||||
}
|
||||
}
|
||||
|
||||
func isContextDone(ctx context.Context) bool {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func addManifestCallToPool(ctx context.Context, p *requestsPool, url, username, password, imageName,
|
||||
tagName, outputFormat string, c chan imageListResult, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
|
||||
resultManifest := manifestResponse{}
|
||||
|
||||
manifestEndpoint, err := combineServerAndEndpointURL(url, fmt.Sprintf("/v2/%s/manifests/%s", imageName, tagName))
|
||||
if err != nil {
|
||||
if isContextDone(ctx) {
|
||||
return
|
||||
}
|
||||
c <- imageListResult{"", err}
|
||||
}
|
||||
|
||||
job := manifestJob{
|
||||
url: manifestEndpoint,
|
||||
username: username,
|
||||
imageName: imageName,
|
||||
password: password,
|
||||
tagName: tagName,
|
||||
manifestResp: resultManifest,
|
||||
outputFormat: outputFormat,
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
p.submitJob(&job)
|
||||
}
|
||||
|
||||
type tagListResp struct {
|
||||
Name string `json:"name"`
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
|
||||
type imageStruct struct {
|
||||
Name string `json:"name"`
|
||||
Tags []tags `json:"tags"`
|
||||
}
|
||||
type tags struct {
|
||||
Name string `json:"name"`
|
||||
Size uint64 `json:"size"`
|
||||
Digest string `json:"digest"`
|
||||
}
|
||||
|
||||
func (img imageStruct) string(format string) (string, error) {
|
||||
switch strings.ToLower(format) {
|
||||
case "", "text":
|
||||
return img.stringPlainText()
|
||||
case "json":
|
||||
return img.stringJSON()
|
||||
case "yml", "yaml":
|
||||
return img.stringYAML()
|
||||
default:
|
||||
return "", ErrInvalidOutputFormat
|
||||
}
|
||||
}
|
||||
|
||||
func (img imageStruct) stringPlainText() (string, error) {
|
||||
var builder strings.Builder
|
||||
|
||||
table := getNoBorderTableWriter(&builder)
|
||||
|
||||
for _, tag := range img.Tags {
|
||||
imageName := ellipsize(img.Name, imageNameWidth, ellipsis)
|
||||
tagName := ellipsize(tag.Name, tagWidth, ellipsis)
|
||||
digest := ellipsize(tag.Digest, digestWidth, "")
|
||||
size := ellipsize(strings.ReplaceAll(humanize.Bytes(tag.Size), " ", ""), sizeWidth, ellipsis)
|
||||
row := []string{imageName,
|
||||
tagName,
|
||||
digest,
|
||||
size,
|
||||
}
|
||||
|
||||
table.Append(row)
|
||||
}
|
||||
|
||||
table.Render()
|
||||
|
||||
return builder.String(), nil
|
||||
}
|
||||
|
||||
func (img imageStruct) stringJSON() (string, error) {
|
||||
var json = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
body, err := json.MarshalIndent(img, "", " ")
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(body), nil
|
||||
}
|
||||
|
||||
func (img imageStruct) stringYAML() (string, error) {
|
||||
body, err := yaml.Marshal(&img)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(body), nil
|
||||
}
|
||||
|
||||
type catalogResponse struct {
|
||||
Repositories []string `json:"repositories"`
|
||||
}
|
||||
|
||||
type manifestResponse struct {
|
||||
Layers []struct {
|
||||
MediaType string `json:"mediaType"`
|
||||
Digest string `json:"digest"`
|
||||
Size uint64 `json:"size"`
|
||||
} `json:"layers"`
|
||||
Annotations struct {
|
||||
WsTychoStackerStackerYaml string `json:"ws.tycho.stacker.stacker_yaml"`
|
||||
WsTychoStackerGitVersion string `json:"ws.tycho.stacker.git_version"`
|
||||
} `json:"annotations"`
|
||||
Config struct {
|
||||
Size int `json:"size"`
|
||||
Digest string `json:"digest"`
|
||||
MediaType string `json:"mediaType"`
|
||||
} `json:"config"`
|
||||
SchemaVersion int `json:"schemaVersion"`
|
||||
}
|
||||
|
||||
func combineServerAndEndpointURL(serverURL, endPoint string) (string, error) {
|
||||
if !isURL(serverURL) {
|
||||
return "", zotErrors.ErrInvalidURL
|
||||
}
|
||||
|
||||
newURL, err := url.Parse(serverURL)
|
||||
|
||||
if err != nil {
|
||||
return "", zotErrors.ErrInvalidURL
|
||||
}
|
||||
|
||||
newURL, _ = newURL.Parse(endPoint)
|
||||
|
||||
return newURL.String(), nil
|
||||
}
|
||||
|
||||
func ellipsize(text string, max int, trailing string) string {
|
||||
if len(text) <= max {
|
||||
return text
|
||||
}
|
||||
|
||||
chopLength := len(trailing)
|
||||
|
||||
return text[:max-chopLength] + trailing
|
||||
}
|
||||
|
||||
func getNoBorderTableWriter(writer io.Writer) *tablewriter.Table {
|
||||
table := tablewriter.NewWriter(writer)
|
||||
|
||||
table.SetAutoWrapText(false)
|
||||
table.SetAutoFormatHeaders(true)
|
||||
table.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
|
||||
table.SetAlignment(tablewriter.ALIGN_LEFT)
|
||||
table.SetCenterSeparator("")
|
||||
table.SetColumnSeparator("")
|
||||
table.SetRowSeparator("")
|
||||
table.SetHeaderLine(false)
|
||||
table.SetBorder(false)
|
||||
table.SetTablePadding(" ")
|
||||
table.SetNoWhiteSpace(true)
|
||||
table.SetColMinWidth(colImageNameIndex, imageNameWidth)
|
||||
table.SetColMinWidth(colTagIndex, tagWidth)
|
||||
table.SetColMinWidth(colDigestIndex, digestWidth)
|
||||
table.SetColMinWidth(colSizeIndex, sizeWidth)
|
||||
|
||||
return table
|
||||
}
|
||||
|
||||
const (
|
||||
imageNameWidth = 32
|
||||
tagWidth = 24
|
||||
digestWidth = 8
|
||||
sizeWidth = 8
|
||||
ellipsis = "..."
|
||||
|
||||
colImageNameIndex = 0
|
||||
colTagIndex = 1
|
||||
colDigestIndex = 2
|
||||
colSizeIndex = 3
|
||||
)
|
Loading…
Reference in a new issue