diff --git a/go.mod b/go.mod index 82e9e9856a..8ab2cbba75 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,6 @@ require ( github.com/docker/go-units v0.4.0 // indirect github.com/fatih/color v1.10.0 // indirect github.com/golang/protobuf v1.4.3 // indirect - github.com/google/go-cmp v0.5.4 // indirect github.com/gorilla/mux v1.8.0 github.com/hokaccha/go-prettyjson v0.0.0-20210113012101-fb4e108d2519 // indirect github.com/kr/text v0.2.0 // indirect @@ -23,6 +22,7 @@ require ( github.com/mitchellh/go-homedir v1.1.0 github.com/mitchellh/go-wordwrap v1.0.1 github.com/moby/term v0.0.0-20201110203204-bea5bbe245bf + github.com/montanaflynn/stats v0.6.5 // indirect github.com/morikuni/aec v1.0.0 // indirect github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/opencontainers/go-digest v1.0.0 // indirect @@ -41,6 +41,7 @@ require ( google.golang.org/grpc v1.35.0 // indirect gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect gopkg.in/yaml.v2 v2.4.0 + gotest.tools/gotestsum v1.6.3 // indirect gotest.tools/v3 v3.0.3 // indirect ) diff --git a/go.sum b/go.sum index 7bcd33eda2..856e6dc267 100644 --- a/go.sum +++ b/go.sum @@ -55,6 +55,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/dnephin/pflag v1.0.7 h1:oxONGlWxhmUct0YzKTgrpQv9AUA1wtPBn7zuSjJqptk= +github.com/dnephin/pflag v1.0.7/go.mod h1:uxE91IoWURlOiTUIA8Mq5ZZkAv3dPUfZNaT80Zm7OQE= github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug= github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v20.10.5+incompatible h1:o5WL5onN4awYGwrW7+oTn5x9AF2prw7V0Ox8ZEkoCdg= @@ -74,6 +76,8 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg= github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= @@ -111,10 +115,13 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= @@ -150,6 +157,8 @@ github.com/hokaccha/go-prettyjson v0.0.0-20210113012101-fb4e108d2519/go.mod h1:p github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ= +github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= @@ -200,6 +209,8 @@ github.com/moby/term v0.0.0-20201110203204-bea5bbe245bf h1:Un6PNx5oMK6CCwO3QTUyP github.com/moby/term v0.0.0-20201110203204-bea5bbe245bf/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/montanaflynn/stats v0.6.5 h1:FhV+8hkLRa1fUu6E93WI5ru9FpccbVZYg1Cfefw0D2A= +github.com/montanaflynn/stats v0.6.5/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= @@ -279,6 +290,7 @@ github.com/xeonx/timeago v1.0.0-rc4/go.mod h1:qDLrYEFynLO7y5Ho7w3GwgtYgpy5UfhcXI github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= @@ -290,6 +302,8 @@ golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 h1:/ZScEX8SfEmUGRHs0gxpqteO5nfNW6axyZbBdw9A12g= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -310,6 +324,8 @@ golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -325,6 +341,7 @@ golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210224082022-3d97a244fca7 h1:OgUuv8lsRpBibGNbSizVwKWlysjaNzmC9gYMhPVfqFM= golang.org/x/net v0.0.0-20210224082022-3d97a244fca7/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -335,6 +352,8 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/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-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 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= @@ -349,11 +368,14 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/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-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210223095934-7937bea0104d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210324051608-47abb6519492 h1:Paq34FxTluEPvVyayQqMPgHm+vTOrIifmcYxFBx9TLg= golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -389,8 +411,13 @@ golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgw golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc h1:NCy3Ohtk6Iny5V/reW2Ktypo4zIpWBdRJ1uFMjBxdg8= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -450,6 +477,8 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/gotestsum v1.6.3 h1:E3wOF4wmxKA19BB5wTY7t0L1m+QNARtDcBX4yqG6DEc= +gotest.tools/gotestsum v1.6.3/go.mod h1:fTR9ZhxC/TLAAx2/WMk/m3TkMB9eEI89gdEzhiRVJT8= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= diff --git a/pkg/cli/benchmark.go b/pkg/cli/benchmark.go new file mode 100644 index 0000000000..e8b3df41e9 --- /dev/null +++ b/pkg/cli/benchmark.go @@ -0,0 +1,137 @@ +package cli + +import ( + "fmt" + "os" + "text/tabwriter" + + "github.com/montanaflynn/stats" + "github.com/spf13/cobra" + + "github.com/replicate/cog/pkg/client" + "github.com/replicate/cog/pkg/console" + "github.com/replicate/cog/pkg/logger" + "github.com/replicate/cog/pkg/model" + "github.com/replicate/cog/pkg/serving" +) + +var benchmarkSetups int +var benchmarkRuns int + +type BenchmarkResults struct { + SetupTimes []float64 + RunTimes []float64 +} + +func newBenchmarkCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "benchmark", + Short: "Measure setup and runtime of package, using the first example from config", + RunE: benchmarkPackage, + Args: cobra.ExactArgs(1), + } + + cmd.Flags().IntVarP(&benchmarkSetups, "setup-iterations", "s", 3, "Number of setup iterations") + cmd.Flags().IntVarP(&benchmarkRuns, "run-iterations", "r", 3, "Number of run iterations per setup iteration") + + return cmd +} + +func benchmarkPackage(cmd *cobra.Command, args []string) error { + repo, err := getRepo() + if err != nil { + return err + } + id := args[0] + + cli := client.NewClient() + console.Info("Starting benchmark of %s:%s", repo, id) + + pkg, err := cli.GetPackage(repo, id) + if err != nil { + return err + } + if len(pkg.Config.Examples) == 0 { + return fmt.Errorf("Package has no examples, cannot run benchmark") + } + + pkgDir, err := os.MkdirTemp("/tmp", "benchmark") + if err != nil { + return err + } + defer os.RemoveAll(pkgDir) + if err := cli.DownloadPackage(repo, id, pkgDir); err != nil { + return err + } + results := new(BenchmarkResults) + for i := 0; i < benchmarkSetups; i++ { + console.Info("Running setup iteration %d", i+1) + if err := runBenchmarkInference(pkg, pkgDir, results, benchmarkRuns); err != nil { + return err + } + } + w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + fmt.Fprintf(w, "Total setups:\t%d\n", benchmarkSetups) + fmt.Fprintf(w, "Total runs:\t%d\n", benchmarkSetups*benchmarkRuns) + w.Flush() + + w = tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + fmt.Fprintln(w, "STEP\tAVERAGE\tMIN\tMAX") + averageSetup, minSetup, maxSetup := benchmarkStats(results.SetupTimes) + fmt.Fprintf(w, "Setup\t%.3f\t%.3f\t%.3f\n", averageSetup, minSetup, maxSetup) + averageRun, minRun, maxRun := benchmarkStats(results.RunTimes) + fmt.Fprintf(w, "Run\t%.3f\t%.3f\t%.3f\n", averageRun, minRun, maxRun) + w.Flush() + return nil +} + +func runBenchmarkInference(pkg *model.Model, pkgDir string, results *BenchmarkResults, runIterations int) error { + servingPlatform, err := serving.NewLocalDockerPlatform() + if err != nil { + return err + } + + example := pkg.Config.Examples[0] + input := serving.NewExampleWithBaseDir(example.Input, pkgDir) + + logWriter := logger.NewConsoleLogger() + deployment, err := servingPlatform.Deploy(pkg, model.TargetDockerCPU, logWriter) + if err != nil { + return err + } + defer func() { + if err := deployment.Undeploy(); err != nil { + console.Warn("Failed to kill Docker container: %s", err) + } + }() + + for i := 0; i < runIterations; i++ { + result, err := deployment.RunInference(input, logWriter) + if err != nil { + return err + } + if i == 0 { + results.SetupTimes = append(results.SetupTimes, result.SetupTime) + } + results.RunTimes = append(results.RunTimes, result.RunTime) + } + + return nil +} + +func benchmarkStats(values []float64) (mean, min, max float64) { + var err error + mean, err = stats.Mean(values) + if err != nil { + panic(err) + } + min, err = stats.Min(values) + if err != nil { + panic(err) + } + max, err = stats.Max(values) + if err != nil { + panic(err) + } + return mean, min, max +} diff --git a/pkg/cli/download.go b/pkg/cli/download.go index 59077fe439..ccd656782d 100644 --- a/pkg/cli/download.go +++ b/pkg/cli/download.go @@ -33,7 +33,6 @@ func downloadPackage(cmd *cobra.Command, args []string) (err error) { if err != nil { return err } - id := args[0] downloadOutputDir, err = homedir.Expand(downloadOutputDir) diff --git a/pkg/cli/infer.go b/pkg/cli/infer.go index 4bff849e5b..04485fd076 100644 --- a/pkg/cli/infer.go +++ b/pkg/cli/infer.go @@ -62,6 +62,7 @@ func cmdInfer(cmd *cobra.Command, args []string) error { return err } logWriter := logger.NewConsoleLogger() + // TODO(andreas): GPU inference deployment, err := servingPlatform.Deploy(pkg, model.TargetDockerCPU, logWriter) if err != nil { return err diff --git a/pkg/cli/list.go b/pkg/cli/list.go index 8378f925d2..1d91dbc42f 100644 --- a/pkg/cli/list.go +++ b/pkg/cli/list.go @@ -3,6 +3,7 @@ package cli import ( "fmt" "os" + "sort" "text/tabwriter" "github.com/spf13/cobra" @@ -36,6 +37,10 @@ func listPackages(cmd *cobra.Command, args []string) error { return err } + sort.Slice(models, func(i, j int) bool { + return models[i].Created.After(models[j].Created) + }) + w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) fmt.Fprintln(w, "ID\tCREATED") for _, mod := range models { diff --git a/pkg/cli/root.go b/pkg/cli/root.go index ccbb6f1115..5bbc0551ae 100644 --- a/pkg/cli/root.go +++ b/pkg/cli/root.go @@ -42,6 +42,7 @@ func NewRootCommand() (*cobra.Command, error) { newRepoCommand(), newDownloadCommand(), newListCommand(), + newBenchmarkCommand(), ) return &rootCmd, nil @@ -72,7 +73,7 @@ func getRepo() (*model.Repo, error) { if projectSettings.Repo != nil { return projectSettings.Repo, nil } - return nil, fmt.Errorf("Either you must run cog repo set in the current directory, or specify --repo/-r") + return nil, fmt.Errorf("No repository specified. Either you must run `cog repo set ` in the current directory, or pass --repo to the command") } } diff --git a/pkg/client/delete.go b/pkg/client/delete.go new file mode 100644 index 0000000000..41da67c35e --- /dev/null +++ b/pkg/client/delete.go @@ -0,0 +1,26 @@ +package client + +import ( + "fmt" + "net/http" + + "github.com/replicate/cog/pkg/model" +) + +// The URL says "package" but the code says "Model", sob +func (c *Client) DeletePackage(repo *model.Repo, id string) error { + url := fmt.Sprintf("http://%s/v1/repos/%s/%s/packages/%s", repo.Host, repo.User, repo.Name, id) + req, err := http.NewRequest(http.MethodDelete, url, nil) + if err != nil { + return err + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("Server returned status %d", resp.StatusCode) + } + return nil +} diff --git a/pkg/docker/cog.py b/pkg/docker/cog.py index 29d5c8c1bb..9819fdd9a8 100644 --- a/pkg/docker/cog.py +++ b/pkg/docker/cog.py @@ -1,3 +1,4 @@ +import time import sys from contextlib import contextmanager import os @@ -11,7 +12,7 @@ from pathlib import Path from typing import Optional, Any, Type, List, Callable, Dict -from flask import Flask, send_file, request, jsonify, abort +from flask import Flask, send_file, request, jsonify, abort, Response from werkzeug.datastructures import FileStorage # TODO(andreas): handle directory input @@ -40,11 +41,15 @@ def cli_run(self): print(result) def make_app(self) -> Flask: + start_time = time.time() self.setup() app = Flask(__name__) + setup_time = time.time() - start_time @app.route("/infer", methods=["POST"]) def handle_request(): + start_time = time.time() + cleanup_functions = [] try: raw_inputs = {} @@ -68,7 +73,8 @@ def handle_request(): inputs = raw_inputs result = self.run(**inputs) - return self.create_response(result) + run_time = time.time() - start_time + return self.create_response(result, setup_time, run_time) finally: for cleanup_function in cleanup_functions: try: @@ -102,12 +108,16 @@ def start_server(self): app = self.make_app() app.run(host="0.0.0.0", port=5000) - def create_response(self, result): + def create_response(self, result, setup_time, run_time): if isinstance(result, Path): - return send_file(str(result)) + resp = send_file(str(result)) elif isinstance(result, str): - return result - return jsonify(result) + resp = Response(result) + else: + resp = jsonify(result) + resp.headers["X-Setup-Time"] = setup_time + resp.headers["X-Run-Time"] = run_time + return resp def validate_and_convert_inputs( self, raw_inputs: Dict[str, Any], cleanup_functions: List[Callable] diff --git a/pkg/docker/cog_test.py b/pkg/docker/cog_test.py index d59eb40087..efdf6855ce 100644 --- a/pkg/docker/cog_test.py +++ b/pkg/docker/cog_test.py @@ -1,5 +1,6 @@ # pytest cog_test.py +import time import shutil import tempfile import io @@ -319,7 +320,7 @@ def run(self, text, num1, num2, path): "num2": { "type": "int", "help": "Second number", - "default": 10, + "default": "10", }, "path": { "type": "Path", @@ -329,6 +330,34 @@ def run(self, text, num1, num2, path): } +def test_timing(): + class ModelSlow(cog.Model): + def setup(self): + time.sleep(0.5) + + def run(self): + time.sleep(0.5) + return "" + + class ModelFast(cog.Model): + def setup(self): + pass + + def run(self): + return "" + + client = make_client(ModelSlow()) + resp = client.post("/infer") + assert resp.status_code == 200 + assert 0.5 < float(resp.headers["X-Setup-Time"]) < 1.0 + assert 0.5 < float(resp.headers["X-Run-Time"]) < 1.0 + + client = make_client(ModelFast()) + resp = client.post("/infer") + assert resp.status_code == 200 + assert float(resp.headers["X-Setup-Time"]) < 0.5 + assert float(resp.headers["X-Run-Time"]) < 0.5 + def test_unzip_to_tempdir(tmpdir_factory): input_dir = tmpdir_factory.mktemp("input") with open(os.path.join(input_dir, "hello.txt"), "w") as f: diff --git a/pkg/server/build.go b/pkg/server/build.go new file mode 100644 index 0000000000..3384d53833 --- /dev/null +++ b/pkg/server/build.go @@ -0,0 +1,218 @@ +package server + +import ( + "crypto/sha1" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "strings" + "time" + + "github.com/mholt/archiver/v3" + + "github.com/replicate/cog/pkg/console" + "github.com/replicate/cog/pkg/docker" + "github.com/replicate/cog/pkg/global" + "github.com/replicate/cog/pkg/logger" + "github.com/replicate/cog/pkg/model" + "github.com/replicate/cog/pkg/serving" +) + +func (s *Server) ReceiveFile(w http.ResponseWriter, r *http.Request) { + user, name, _ := getRepoVars(r) + + console.Info("Received build request") + streamLogger := logger.NewStreamLogger(w) + mod, err := s.ReceiveModel(r, streamLogger, user, name) + if err != nil { + streamLogger.WriteError(err) + console.Error(err.Error()) + return + } + streamLogger.WriteModel(mod) +} + +func (s *Server) ReceiveModel(r *http.Request, streamLogger *logger.StreamLogger, user string, name string) (*model.Model, error) { + // max 5GB models + if err := r.ParseMultipartForm(5 << 30); err != nil { + return nil, fmt.Errorf("Failed to parse request: %w", err) + } + file, header, err := r.FormFile("file") + if err != nil { + return nil, fmt.Errorf("Failed to read input file: %w", err) + } + defer file.Close() + + streamLogger.WriteStatus("Received model") + + hasher := sha1.New() + if _, err := io.Copy(hasher, file); err != nil { + return nil, fmt.Errorf("Failed to compute hash: %w", err) + } + id := fmt.Sprintf("%x", hasher.Sum(nil)) + + parentDir, err := os.MkdirTemp("/tmp", "unzip") + if err != nil { + return nil, fmt.Errorf("Failed to make tempdir: %w", err) + } + dir := filepath.Join(parentDir, topLevelSourceDir) + if err := os.Mkdir(dir, 0755); err != nil { + return nil, fmt.Errorf("Failed to make source dir: %w", err) + } + z := archiver.Zip{} + if err := z.ReaderUnarchive(file, header.Size, dir); err != nil { + return nil, fmt.Errorf("Failed to unzip: %w", err) + } + + configRaw, err := os.ReadFile(filepath.Join(dir, global.ConfigFilename)) + if err != nil { + return nil, fmt.Errorf("Failed to read %s: %w", global.ConfigFilename, err) + } + config, err := model.ConfigFromYAML(configRaw) + if err != nil { + return nil, err + } + + if err := config.ValidateAndCompleteConfig(); err != nil { + return nil, err + } + + if _, err := file.Seek(0, io.SeekStart); err != nil { + return nil, fmt.Errorf("Failed to rewind file: %w", err) + } + if err := s.store.Upload(user, name, id, file); err != nil { + return nil, fmt.Errorf("Failed to upload to storage: %w", err) + } + + artifacts, err := s.buildDockerImages(dir, config, name, streamLogger) + if err != nil { + return nil, err + } + mod := &model.Model{ + ID: id, + Artifacts: artifacts, + Config: config, + Created: time.Now(), + } + + runArgs, err := s.testModel(mod, dir, streamLogger) + if err != nil { + // TODO(andreas): return other response than 500 if validation fails + return nil, err + } + mod.RunArguments = runArgs + + streamLogger.WriteStatus("Inserting into database") + if err := s.db.InsertModel(user, name, id, mod); err != nil { + return nil, fmt.Errorf("Failed to insert into database: %w", err) + } + + return mod, nil +} + +func (s *Server) testModel(mod *model.Model, dir string, logWriter logger.Logger) (map[string]*model.RunArgument, error) { + logWriter.WriteStatus("Testing model") + deployment, err := s.servingPlatform.Deploy(mod, model.TargetDockerCPU, logWriter) + if err != nil { + return nil, err + } + defer deployment.Undeploy() + + help, err := deployment.Help(logWriter) + if err != nil { + return nil, err + } + + for _, example := range mod.Config.Examples { + if err := validateServingExampleInput(help, example.Input); err != nil { + return nil, fmt.Errorf("Example input doesn't match run arguments: %w", err) + } + input := serving.NewExampleWithBaseDir(example.Input, dir) + + result, err := deployment.RunInference(input, logWriter) + if err != nil { + return nil, err + } + output := result.Values["output"] + outputBytes, err := io.ReadAll(output.Buffer) + if err != nil { + return nil, fmt.Errorf("Failed to read output: %w", err) + } + logWriter.WriteLogLine(fmt.Sprintf("Inference result length: %d, mime type: %s", len(outputBytes), output.MimeType)) + if example.Output != "" && strings.TrimSpace(string(outputBytes)) != example.Output { + return nil, fmt.Errorf("Output %s doesn't match expected: %s", outputBytes, example.Output) + } + } + + return help.Arguments, nil +} + +// TODO(andreas): include user in docker image name? +func (s *Server) buildDockerImages(dir string, config *model.Config, name string, logWriter logger.Logger) ([]*model.Artifact, error) { + // TODO(andreas): parallelize + artifacts := []*model.Artifact{} + for _, arch := range config.Environment.Architectures { + + logWriter.WriteStatus("Building %s image", arch) + + generator := &docker.DockerfileGenerator{Config: config, Arch: arch} + dockerfileContents, err := generator.Generate() + if err != nil { + return nil, fmt.Errorf("Failed to generate Dockerfile for %s: %w", arch, err) + } + // TODO(andreas): pipe dockerfile contents to builder + relDockerfilePath := "Dockerfile." + arch + dockerfilePath := filepath.Join(dir, relDockerfilePath) + if err := os.WriteFile(dockerfilePath, []byte(dockerfileContents), 0644); err != nil { + return nil, fmt.Errorf("Failed to write Dockerfile for %s", arch) + } + + tag, err := s.dockerImageBuilder.BuildAndPush(dir, relDockerfilePath, name, logWriter) + if err != nil { + return nil, fmt.Errorf("Failed to build Docker image: %w", err) + } + var target model.Target + switch arch { + case "cpu": + target = model.TargetDockerCPU + case "gpu": + target = model.TargetDockerGPU + } + artifacts = append(artifacts, &model.Artifact{ + Target: target, + URI: tag, + }) + + } + return artifacts, nil +} + +func validateServingExampleInput(help *serving.HelpResponse, input map[string]string) error { + // TODO(andreas): validate types + missingNames := []string{} + extraneousNames := []string{} + + for name, arg := range help.Arguments { + if _, ok := input[name]; !ok && arg.Default == nil { + missingNames = append(missingNames, name) + } + } + for name := range input { + if _, ok := help.Arguments[name]; !ok { + extraneousNames = append(extraneousNames, name) + } + } + errParts := []string{} + if len(missingNames) > 0 { + errParts = append(errParts, "Missing arguments: "+strings.Join(missingNames, ", ")) + } + if len(extraneousNames) > 0 { + errParts = append(errParts, "Extraneous arguments: "+strings.Join(extraneousNames, ", ")) + } + if len(errParts) > 0 { + return fmt.Errorf(strings.Join(errParts, "; ")) + } + return nil +} diff --git a/pkg/server/delete.go b/pkg/server/delete.go new file mode 100644 index 0000000000..414d6c9545 --- /dev/null +++ b/pkg/server/delete.go @@ -0,0 +1,36 @@ +package server + +import ( + "net/http" + + "github.com/replicate/cog/pkg/console" +) + +func (s *Server) DeletePackage(w http.ResponseWriter, r *http.Request) { + user, name, id := getRepoVars(r) + console.Info("Received delete request for %s/%s/%s", user, name, id) + + mod, err := s.db.GetModel(user, name, id) + if err != nil { + console.Error(err.Error()) + w.WriteHeader(http.StatusInternalServerError) + return + } + if mod == nil { + w.WriteHeader(http.StatusNotFound) + return + } + + if err := s.store.Delete(user, name, id); err != nil { + console.Error(err.Error()) + w.WriteHeader(http.StatusInternalServerError) + return + } + if err := s.db.DeleteModel(user, name, id); err != nil { + console.Error(err.Error()) + w.WriteHeader(http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) + w.Write([]byte("Deleted " + id)) +} diff --git a/pkg/server/download.go b/pkg/server/download.go new file mode 100644 index 0000000000..ed6b527a03 --- /dev/null +++ b/pkg/server/download.go @@ -0,0 +1,35 @@ +package server + +import ( + "bytes" + "net/http" + "time" + + "github.com/replicate/cog/pkg/console" +) + +func (s *Server) SendModelPackage(w http.ResponseWriter, r *http.Request) { + user, name, id := getRepoVars(r) + console.Info("Received download request for %s/%s/%s", user, name, id) + modTime := time.Now() // TODO + + mod, err := s.db.GetModel(user, name, id) + if err != nil { + console.Error(err.Error()) + w.WriteHeader(http.StatusInternalServerError) + return + } + if mod == nil { + w.WriteHeader(http.StatusNotFound) + return + } + + content, err := s.store.Download(user, name, id) + if err != nil { + console.Error(err.Error()) + w.WriteHeader(http.StatusInternalServerError) + return + } + console.Info("Downloaded %d bytes", len(content)) + http.ServeContent(w, r, id+".zip", modTime, bytes.NewReader(content)) +} diff --git a/pkg/server/get.go b/pkg/server/get.go new file mode 100644 index 0000000000..4a7ee3d140 --- /dev/null +++ b/pkg/server/get.go @@ -0,0 +1,32 @@ +package server + +import ( + "encoding/json" + "net/http" + + "github.com/replicate/cog/pkg/console" +) + +func (s *Server) SendModelMetadata(w http.ResponseWriter, r *http.Request) { + user, name, id := getRepoVars(r) + console.Info("Received get request for %s/%s/%s", user, name, id) + + mod, err := s.db.GetModel(user, name, id) + if err != nil { + console.Error(err.Error()) + w.WriteHeader(http.StatusInternalServerError) + return + } + if mod == nil { + w.WriteHeader(http.StatusNotFound) + return + } + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "application/json") + + if err := json.NewEncoder(w).Encode(mod); err != nil { + console.Error(err.Error()) + w.WriteHeader(http.StatusInternalServerError) + return + } +} diff --git a/pkg/server/list.go b/pkg/server/list.go new file mode 100644 index 0000000000..9d88e39865 --- /dev/null +++ b/pkg/server/list.go @@ -0,0 +1,28 @@ +package server + +import ( + "encoding/json" + "net/http" + + "github.com/replicate/cog/pkg/console" +) + +func (s *Server) ListPackages(w http.ResponseWriter, r *http.Request) { + user, name, _ := getRepoVars(r) + console.Info("Received list request for %s%s", user, name) + + models, err := s.db.ListModels(user, name) + if err != nil { + console.Error(err.Error()) + w.WriteHeader(http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "application/json") + + if err := json.NewEncoder(w).Encode(models); err != nil { + console.Error(err.Error()) + w.WriteHeader(http.StatusInternalServerError) + return + } +} diff --git a/pkg/server/server.go b/pkg/server/server.go index da905ac728..e41125d8a7 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -1,27 +1,14 @@ package server import ( - "bytes" - "crypto/sha1" - "encoding/json" "fmt" - "io" "net/http" - "os" - "path/filepath" - "strings" - "time" "github.com/gorilla/mux" - "github.com/mholt/archiver/v3" "github.com/replicate/cog/pkg/console" - "github.com/replicate/cog/pkg/database" "github.com/replicate/cog/pkg/docker" - "github.com/replicate/cog/pkg/global" - "github.com/replicate/cog/pkg/logger" - "github.com/replicate/cog/pkg/model" "github.com/replicate/cog/pkg/serving" "github.com/replicate/cog/pkg/storage" ) @@ -76,302 +63,6 @@ func (s *Server) Start() error { return http.ListenAndServe(fmt.Sprintf(":%d", s.port), router) } -func (s *Server) ReceiveFile(w http.ResponseWriter, r *http.Request) { - user, name, _ := getRepoVars(r) - - console.Info("Received build request") - streamLogger := logger.NewStreamLogger(w) - mod, err := s.ReceiveModel(r, streamLogger, user, name) - if err != nil { - streamLogger.WriteError(err) - console.Error(err.Error()) - return - } - streamLogger.WriteModel(mod) -} - -func (s *Server) SendModelPackage(w http.ResponseWriter, r *http.Request) { - user, name, id := getRepoVars(r) - console.Info("Received download request for %s/%s/%s", user, name, id) - modTime := time.Now() // TODO - - mod, err := s.db.GetModel(user, name, id) - if err != nil { - console.Error(err.Error()) - w.WriteHeader(http.StatusInternalServerError) - return - } - if mod == nil { - w.WriteHeader(http.StatusNotFound) - return - } - - content, err := s.store.Download(user, name, id) - if err != nil { - console.Error(err.Error()) - w.WriteHeader(http.StatusInternalServerError) - return - } - console.Info("Downloaded %d bytes", len(content)) - http.ServeContent(w, r, id+".zip", modTime, bytes.NewReader(content)) -} - -func (s *Server) SendModelMetadata(w http.ResponseWriter, r *http.Request) { - user, name, id := getRepoVars(r) - console.Info("Received get request for %s/%s/%s", user, name, id) - - mod, err := s.db.GetModel(user, name, id) - if err != nil { - console.Error(err.Error()) - w.WriteHeader(http.StatusInternalServerError) - return - } - if mod == nil { - w.WriteHeader(http.StatusNotFound) - return - } - w.WriteHeader(http.StatusOK) - w.Header().Set("Content-Type", "application/json") - - if err := json.NewEncoder(w).Encode(mod); err != nil { - console.Error(err.Error()) - w.WriteHeader(http.StatusInternalServerError) - return - } -} - -func (s *Server) ListPackages(w http.ResponseWriter, r *http.Request) { - user, name, _ := getRepoVars(r) - console.Info("Received list request for %s%s", user, name) - - models, err := s.db.ListModels(user, name) - if err != nil { - console.Error(err.Error()) - w.WriteHeader(http.StatusInternalServerError) - return - } - w.WriteHeader(http.StatusOK) - w.Header().Set("Content-Type", "application/json") - - if err := json.NewEncoder(w).Encode(models); err != nil { - console.Error(err.Error()) - w.WriteHeader(http.StatusInternalServerError) - return - } -} - -func (s *Server) DeletePackage(w http.ResponseWriter, r *http.Request) { - user, name, id := getRepoVars(r) - console.Info("Received delete request for %s/%s/%s", user, name, id) - - mod, err := s.db.GetModel(user, name, id) - if err != nil { - console.Error(err.Error()) - w.WriteHeader(http.StatusInternalServerError) - return - } - if mod == nil { - w.WriteHeader(http.StatusNotFound) - return - } - - if err := s.store.Delete(user, name, id); err != nil { - console.Error(err.Error()) - w.WriteHeader(http.StatusInternalServerError) - return - } - if err := s.db.DeleteModel(user, name, id); err != nil { - console.Error(err.Error()) - w.WriteHeader(http.StatusInternalServerError) - return - } - w.WriteHeader(http.StatusOK) - w.Write([]byte("Deleted " + id)) -} - -func (s *Server) ReceiveModel(r *http.Request, streamLogger *logger.StreamLogger, user string, name string) (*model.Model, error) { - // max 5GB models - if err := r.ParseMultipartForm(5 << 30); err != nil { - return nil, fmt.Errorf("Failed to parse request: %w", err) - } - file, header, err := r.FormFile("file") - if err != nil { - return nil, fmt.Errorf("Failed to read input file: %w", err) - } - defer file.Close() - - streamLogger.WriteStatus("Received model") - - hasher := sha1.New() - if _, err := io.Copy(hasher, file); err != nil { - return nil, fmt.Errorf("Failed to compute hash: %w", err) - } - id := fmt.Sprintf("%x", hasher.Sum(nil)) - - parentDir, err := os.MkdirTemp("/tmp", "unzip") - if err != nil { - return nil, fmt.Errorf("Failed to make tempdir: %w", err) - } - dir := filepath.Join(parentDir, topLevelSourceDir) - if err := os.Mkdir(dir, 0755); err != nil { - return nil, fmt.Errorf("Failed to make source dir: %w", err) - } - z := archiver.Zip{} - if err := z.ReaderUnarchive(file, header.Size, dir); err != nil { - return nil, fmt.Errorf("Failed to unzip: %w", err) - } - - configRaw, err := os.ReadFile(filepath.Join(dir, global.ConfigFilename)) - if err != nil { - return nil, fmt.Errorf("Failed to read %s: %w", global.ConfigFilename, err) - } - config, err := model.ConfigFromYAML(configRaw) - if err != nil { - return nil, err - } - - if err := config.ValidateAndCompleteConfig(); err != nil { - return nil, err - } - - if _, err := file.Seek(0, io.SeekStart); err != nil { - return nil, fmt.Errorf("Failed to rewind file: %w", err) - } - if err := s.store.Upload(user, name, id, file); err != nil { - return nil, fmt.Errorf("Failed to upload to storage: %w", err) - } - - artifacts, err := s.buildDockerImages(dir, config, name, streamLogger) - if err != nil { - return nil, err - } - mod := &model.Model{ - ID: id, - Artifacts: artifacts, - Config: config, - Created: time.Now(), - } - - runArgs, err := s.testModel(mod, dir, streamLogger) - if err != nil { - // TODO(andreas): return other response than 500 if validation fails - return nil, err - } - mod.RunArguments = runArgs - - streamLogger.WriteStatus("Inserting into database") - if err := s.db.InsertModel(user, name, id, mod); err != nil { - return nil, fmt.Errorf("Failed to insert into database: %w", err) - } - - return mod, nil -} - -func (s *Server) testModel(mod *model.Model, dir string, logWriter logger.Logger) (map[string]*model.RunArgument, error) { - logWriter.WriteStatus("Testing model") - deployment, err := s.servingPlatform.Deploy(mod, model.TargetDockerCPU, logWriter) - if err != nil { - return nil, err - } - defer deployment.Undeploy() - - help, err := deployment.Help(logWriter) - if err != nil { - return nil, err - } - - for _, example := range mod.Config.Examples { - if err := validateServingExampleInput(help, example.Input); err != nil { - return nil, fmt.Errorf("Example input doesn't match run arguments: %w", err) - } - input := serving.NewExampleWithBaseDir(example.Input, dir) - - result, err := deployment.RunInference(input, logWriter) - if err != nil { - return nil, err - } - output := result.Values["output"] - outputBytes, err := io.ReadAll(output.Buffer) - if err != nil { - return nil, fmt.Errorf("Failed to read output: %w", err) - } - logWriter.WriteLogLine(fmt.Sprintf("Inference result length: %d, mime type: %s", len(outputBytes), output.MimeType)) - if example.Output != "" && string(outputBytes) != example.Output { - return nil, fmt.Errorf("Output %s doesn't match expected: %s", outputBytes, example.Output) - } - } - - return help.Arguments, nil -} - -// TODO(andreas): include user in docker image name? -func (s *Server) buildDockerImages(dir string, config *model.Config, name string, logWriter logger.Logger) ([]*model.Artifact, error) { - // TODO(andreas): parallelize - artifacts := []*model.Artifact{} - for _, arch := range config.Environment.Architectures { - - logWriter.WriteStatus("Building %s image", arch) - - generator := &docker.DockerfileGenerator{Config: config, Arch: arch} - dockerfileContents, err := generator.Generate() - if err != nil { - return nil, fmt.Errorf("Failed to generate Dockerfile for %s: %w", arch, err) - } - // TODO(andreas): pipe dockerfile contents to builder - relDockerfilePath := "Dockerfile." + arch - dockerfilePath := filepath.Join(dir, relDockerfilePath) - if err := os.WriteFile(dockerfilePath, []byte(dockerfileContents), 0644); err != nil { - return nil, fmt.Errorf("Failed to write Dockerfile for %s", arch) - } - - tag, err := s.dockerImageBuilder.BuildAndPush(dir, relDockerfilePath, name, logWriter) - if err != nil { - return nil, fmt.Errorf("Failed to build Docker image: %w", err) - } - var target model.Target - switch arch { - case "cpu": - target = model.TargetDockerCPU - case "gpu": - target = model.TargetDockerGPU - } - artifacts = append(artifacts, &model.Artifact{ - Target: target, - URI: tag, - }) - - } - return artifacts, nil -} - -func validateServingExampleInput(help *serving.HelpResponse, input map[string]string) error { - // TODO(andreas): validate types - missingNames := []string{} - extraneousNames := []string{} - - for name, arg := range help.Arguments { - if _, ok := input[name]; !ok && arg.Default == nil { - missingNames = append(missingNames, name) - } - } - for name := range input { - if _, ok := help.Arguments[name]; !ok { - extraneousNames = append(extraneousNames, name) - } - } - errParts := []string{} - if len(missingNames) > 0 { - errParts = append(errParts, "Missing arguments: "+strings.Join(missingNames, ", ")) - } - if len(extraneousNames) > 0 { - errParts = append(errParts, "Extraneous arguments: "+strings.Join(extraneousNames, ", ")) - } - if len(errParts) > 0 { - return fmt.Errorf(strings.Join(errParts, "; ")) - } - return nil -} - func getRepoVars(r *http.Request) (user string, name string, id string) { vars := mux.Vars(r) return vars["user"], vars["name"], vars["id"] diff --git a/pkg/serving/local.go b/pkg/serving/local.go index 8e9c52771e..faaa9049a7 100644 --- a/pkg/serving/local.go +++ b/pkg/serving/local.go @@ -19,6 +19,7 @@ import ( "github.com/docker/docker/client" "github.com/docker/go-connections/nat" + "github.com/replicate/cog/pkg/console" "github.com/replicate/cog/pkg/docker" "github.com/replicate/cog/pkg/global" "github.com/replicate/cog/pkg/logger" @@ -223,6 +224,23 @@ func (d *LocalDockerDeployment) RunInference(input *Example, logWriter logger.Lo return nil, fmt.Errorf("Failed to read response: %w", err) } + setupTime := -1.0 + runTime := -1.0 + setupTimeStr := resp.Header.Get("X-Setup-Time") + if setupTimeStr != "" { + setupTime, err = strconv.ParseFloat(setupTimeStr, 64) + if err != nil { + console.Error("Failed to parse setup time '%s' as float: %s", setupTimeStr, err) + } + } + runTimeStr := resp.Header.Get("X-Run-Time") + if runTimeStr != "" { + runTime, err = strconv.ParseFloat(runTimeStr, 64) + if err != nil { + console.Error("Failed to parse run time '%s' as float: %s", runTimeStr, err) + } + } + result := &Result{ Values: map[string]ResultValue{ // TODO(andreas): support multiple outputs? @@ -231,6 +249,8 @@ func (d *LocalDockerDeployment) RunInference(input *Example, logWriter logger.Lo MimeType: mimeType, }, }, + SetupTime: setupTime, + RunTime: runTime, } return result, nil } diff --git a/pkg/serving/platform.go b/pkg/serving/platform.go index 9c2cb63202..862cdc8750 100644 --- a/pkg/serving/platform.go +++ b/pkg/serving/platform.go @@ -62,7 +62,9 @@ type ResultValue struct { } type Result struct { - Values map[string]ResultValue + Values map[string]ResultValue + SetupTime float64 + RunTime float64 } type HelpResponse struct {