0
Fork 0
mirror of https://github.com/project-zot/zot.git synced 2024-12-30 22:34:13 -05:00

router: move to gorilla/mux to support multiple name path components

This commit is contained in:
Ramkumar Chinchani 2019-07-09 22:23:59 -07:00
parent 131e19c26d
commit 066bf1b9eb
15 changed files with 660 additions and 418 deletions

View file

@ -9,7 +9,7 @@ binary: doc
.PHONY: debug .PHONY: debug
debug: doc debug: doc
go build -v -gcflags '-N -l' -o bin/zot-debug -tags=jsoniter ./cmd/zot go build -v -gcflags all='-N -l' -o bin/zot-debug -tags=jsoniter ./cmd/zot
.PHONY: test .PHONY: test
test: test:

View file

@ -12,5 +12,4 @@
# Caveats # Caveats
* go 1.12+ * go 1.12+
* Image name consists of only one path component, for example, _busybox:latest_ instead _ubuntu/busybox:latest_
* The OCI distribution spec is still WIP, and we try to keep up * The OCI distribution spec is still WIP, and we try to keep up

View file

@ -575,7 +575,7 @@ go_repository(
go_repository( go_repository(
name = "com_github_smartystreets_goconvey", name = "com_github_smartystreets_goconvey",
commit = "68dc04aab96a", commit = "9d28bd7c0945",
importpath = "github.com/smartystreets/goconvey", importpath = "github.com/smartystreets/goconvey",
) )
@ -648,7 +648,7 @@ go_repository(
go_repository( go_repository(
name = "com_github_swaggo_swag", name = "com_github_swaggo_swag",
importpath = "github.com/swaggo/swag", importpath = "github.com/swaggo/swag",
tag = "v1.5.1", tag = "v1.6.2",
) )
go_repository( go_repository(
@ -660,7 +660,7 @@ go_repository(
go_repository( go_repository(
name = "com_github_ugorji_go", name = "com_github_ugorji_go",
importpath = "github.com/ugorji/go", importpath = "github.com/ugorji/go",
tag = "v1.1.5-pre", tag = "v1.1.4",
) )
go_repository( go_repository(
@ -761,7 +761,7 @@ go_repository(
go_repository( go_repository(
name = "org_golang_x_crypto", name = "org_golang_x_crypto",
commit = "ea8f1a30c443", commit = "4def268fd1a4",
importpath = "golang.org/x/crypto", importpath = "golang.org/x/crypto",
) )
@ -830,3 +830,27 @@ go_repository(
importpath = "go.uber.org/zap", importpath = "go.uber.org/zap",
tag = "v1.10.0", tag = "v1.10.0",
) )
go_repository(
name = "com_github_gorilla_mux",
importpath = "github.com/gorilla/mux",
tag = "v1.7.3",
)
go_repository(
name = "com_github_kylebanks_depth",
importpath = "github.com/KyleBanks/depth",
tag = "v1.2.1",
)
go_repository(
name = "com_github_swaggo_files",
commit = "630677cd5c14",
importpath = "github.com/swaggo/files",
)
go_repository(
name = "com_github_swaggo_http_swagger",
commit = "c2865af9083e",
importpath = "github.com/swaggo/http-swagger",
)

View file

@ -1,6 +1,6 @@
// GENERATED BY THE COMMAND ABOVE; DO NOT EDIT // GENERATED BY THE COMMAND ABOVE; DO NOT EDIT
// This file was generated by swaggo/swag at // This file was generated by swaggo/swag at
// 2019-06-21 14:49:20.043038483 -0700 PDT m=+0.069174432 // 2019-07-10 17:20:00.064076444 -0700 PDT m=+0.118699568
package docs package docs

View file

@ -4,7 +4,7 @@
"rootDirectory":"/tmp/zot" "rootDirectory":"/tmp/zot"
}, },
"http": { "http": {
"address":"127.0.0.1", "address":"0.0.0.0",
"port":"8080" "port":"8080"
}, },
"log":{ "log":{

15
go.mod
View file

@ -4,19 +4,22 @@ go 1.12
require ( require (
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc
github.com/gin-gonic/gin v1.4.0
github.com/gofrs/uuid v3.2.0+incompatible github.com/gofrs/uuid v3.2.0+incompatible
github.com/gorilla/mux v1.7.3
github.com/json-iterator/go v1.1.6
github.com/mitchellh/mapstructure v1.1.2 github.com/mitchellh/mapstructure v1.1.2
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/opencontainers/distribution-spec v1.0.0-rc0 github.com/opencontainers/distribution-spec v1.0.0-rc0
github.com/opencontainers/go-digest v1.0.0-rc1 github.com/opencontainers/go-digest v1.0.0-rc1
github.com/opencontainers/image-spec v1.0.1 github.com/opencontainers/image-spec v1.0.1
github.com/rs/zerolog v1.14.3 github.com/rs/zerolog v1.14.3
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a github.com/smartystreets/goconvey v0.0.0-20190710185942-9d28bd7c0945
github.com/spf13/cobra v0.0.5 github.com/spf13/cobra v0.0.5
github.com/spf13/viper v1.4.0 github.com/spf13/viper v1.4.0
github.com/swaggo/gin-swagger v1.1.0 github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14 // indirect
github.com/swaggo/swag v1.5.1 github.com/swaggo/http-swagger v0.0.0-20190614090009-c2865af9083e
github.com/ugorji/go v1.1.5-pre // indirect github.com/swaggo/swag v1.6.2
golang.org/x/crypto v0.0.0-20190618222545-ea8f1a30c443 golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4
gopkg.in/resty.v1 v1.12.0 gopkg.in/resty.v1 v1.12.0
) )

59
go.sum
View file

@ -1,6 +1,7 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/PuerkitoBio/purell v1.1.0 h1:rmGxhojJlM0tuKtfdvliR84CFHljx9ag64t2xmVkjK4= github.com/PuerkitoBio/purell v1.1.0 h1:rmGxhojJlM0tuKtfdvliR84CFHljx9ag64t2xmVkjK4=
github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
@ -22,29 +23,20 @@ github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/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/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/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 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= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3 h1:t8FVkw33L+wilf2QiWkw0UV77qRpcH/JHPKGpKa2E8g=
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y=
github.com/gin-gonic/gin v1.4.0 h1:3tMoCCfM7ppqsR0ptz/wi1impNpT7/9wQtMZ8lr1mCQ=
github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-openapi/jsonpointer v0.17.0 h1:nH6xp8XdXHx8dqveo0ZuJBluCO2qGrPbDNZ0dwoRHP0= github.com/go-openapi/jsonpointer v0.17.0 h1:nH6xp8XdXHx8dqveo0ZuJBluCO2qGrPbDNZ0dwoRHP0=
github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
github.com/go-openapi/jsonreference v0.18.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
github.com/go-openapi/jsonreference v0.19.0 h1:BqWKpV1dFd+AuiKlgtddwVIFQsuMpxfBDBHGfM2yNpk= github.com/go-openapi/jsonreference v0.19.0 h1:BqWKpV1dFd+AuiKlgtddwVIFQsuMpxfBDBHGfM2yNpk=
github.com/go-openapi/jsonreference v0.19.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= github.com/go-openapi/jsonreference v0.19.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
github.com/go-openapi/spec v0.19.0 h1:A4SZ6IWh3lnjH0rG0Z5lkxazMGBECtrZcbyYQi+64k4= github.com/go-openapi/spec v0.19.0 h1:A4SZ6IWh3lnjH0rG0Z5lkxazMGBECtrZcbyYQi+64k4=
github.com/go-openapi/spec v0.19.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= 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 h1:iqrgMg7Q7SvtbWLlltPrkMs0UBJI6oTSs79JFRUi880=
@ -58,19 +50,18 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfU
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
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= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 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.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs= github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs=
@ -82,18 +73,13 @@ github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvW
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
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.1/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/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.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
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.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329 h1:2gxZ0XQIU/5z3Z3bUBu+FXuk2pFbkN6tcwi/pjyaDic= 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/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.7 h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
@ -115,7 +101,6 @@ github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/9
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
@ -134,8 +119,8 @@ github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= 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 v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a h1:pa8hGb/2YqsZKovtsgrwcDH1RZhVbTKCjLp47XpqCDs= github.com/smartystreets/goconvey v0.0.0-20190710185942-9d28bd7c0945 h1:N8Bg45zpk/UcpNGnfJt2y/3lRWASHNTUET8owPYCgYI=
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/smartystreets/goconvey v0.0.0-20190710185942-9d28bd7c0945/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 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/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= github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
@ -154,23 +139,16 @@ github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/y
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 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.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/swaggo/gin-swagger v1.1.0 h1:ZI6/82S07DkkrMfGKbJhKj1R+QNTICkeAJP06pU36pU= github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14 h1:PyYN9JH5jY9j6av01SpfRMb+1DWg/i3MbGOKPxJ2wjM=
github.com/swaggo/gin-swagger v1.1.0/go.mod h1:FQlm07YuT1glfN3hQiO11UQ2m39vOCZ/aa3WWr5E+XU= github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14/go.mod h1:gxQT6pBGRuIGunNf/+tSOB5OHvguWi8Tbt82WOkf35E=
github.com/swaggo/swag v1.4.0/go.mod h1:hog2WgeMOrQ/LvQ+o1YGTeT+vWVrbi0SiIslBtxKTyM= github.com/swaggo/http-swagger v0.0.0-20190614090009-c2865af9083e h1:m5sYJ43teIUlESuKRFQRRm7kqi6ExiYwVKfoXNuRgHU=
github.com/swaggo/swag v1.5.1 h1:2Agm8I4K5qb00620mHq0VJ05/KT4FtmALPIcQR9lEZM= github.com/swaggo/http-swagger v0.0.0-20190614090009-c2865af9083e/go.mod h1:eycbshptIv+tqTMlLEaGC2noPNcetbrcYEelLafrIDI=
github.com/swaggo/swag v1.5.1/go.mod h1:1Bl9F/ZBpVWh22nY0zmYyASPO1lI/zIwRDrpZU+tv8Y= github.com/swaggo/swag v1.6.2 h1:WQMAtT/FmMBb7g0rAuHDhG3vvdtHKJ3WZ+Ssb0p4Y6E=
github.com/swaggo/swag v1.6.2/go.mod h1:YyZstMc22WYm6GEDx/CYWxq+faBbjQ5EqwQcrjREDBo=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/ugorji/go v1.1.4 h1:j4s+tAvLfL3bZyefP2SEWmhBzmuIlH/eqNuPdFPgngw=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/ugorji/go v1.1.5-pre h1:jyJKFOSEbdOc2HODrf2qcCkYOdq7zzXqA9bhW5oV4fM=
github.com/ugorji/go v1.1.5-pre/go.mod h1:FwP/aQVg39TXzItUBMwnWp9T9gPQnXw4Poh4/oBQZ/0=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2 h1:EICbibRW4JNKMcY+LsWmuwob+CRS1BmdRdjphAm9mH4=
github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/ugorji/go/codec v1.1.5-pre h1:5YV9PsFAN+ndcCtTM7s60no7nY7eTG3LPtxhSwuxzCs=
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.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
@ -182,18 +160,16 @@ go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 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-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190618222545-ea8f1a30c443 h1:IcSOAf4PyMp3U3XbIEj1/xJ2BjNN2jWv7JoyOsMxXUU= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc=
golang.org/x/crypto v0.0.0-20190618222545-ea8f1a30c443/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/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-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco= golang.org/x/net v0.0.0-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@ -207,14 +183,12 @@ golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190110015856-aa033095749b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
@ -227,12 +201,7 @@ google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZi
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ=
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
gopkg.in/resty.v1 v1.12.0 h1:CuXP0Pjfw9rOuY6EP+UvtNvt5DSqHpIxILZKT/quCZI= gopkg.in/resty.v1 v1.12.0 h1:CuXP0Pjfw9rOuY6EP+UvtNvt5DSqHpIxILZKT/quCZI=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=

View file

@ -8,6 +8,7 @@ go_library(
"controller.go", "controller.go",
"errors.go", "errors.go",
"log.go", "log.go",
"regexp.go",
"routes.go", "routes.go",
], ],
importpath = "github.com/anuvu/zot/pkg/api", importpath = "github.com/anuvu/zot/pkg/api",
@ -16,12 +17,12 @@ go_library(
"//docs:go_default_library", "//docs:go_default_library",
"//errors:go_default_library", "//errors:go_default_library",
"//pkg/storage:go_default_library", "//pkg/storage:go_default_library",
"@com_github_gin_gonic_gin//:go_default_library", "@com_github_gorilla_mux//:go_default_library",
"@com_github_json_iterator_go//:go_default_library",
"@com_github_opencontainers_distribution_spec//:go_default_library", "@com_github_opencontainers_distribution_spec//:go_default_library",
"@com_github_opencontainers_image_spec//specs-go/v1:go_default_library", "@com_github_opencontainers_image_spec//specs-go/v1:go_default_library",
"@com_github_rs_zerolog//:go_default_library", "@com_github_rs_zerolog//:go_default_library",
"@com_github_swaggo_gin_swagger//:go_default_library", "@com_github_swaggo_http_swagger//:go_default_library",
"@com_github_swaggo_gin_swagger//swaggerFiles:go_default_library",
"@org_golang_x_crypto//bcrypt:go_default_library", "@org_golang_x_crypto//bcrypt:go_default_library",
], ],
) )

View file

@ -9,20 +9,25 @@ import (
"strings" "strings"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gorilla/mux"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
func authFail(ginCtx *gin.Context, realm string, delay int) { func authFail(w http.ResponseWriter, realm string, delay int) {
time.Sleep(time.Duration(delay) * time.Second) time.Sleep(time.Duration(delay) * time.Second)
ginCtx.Header("WWW-Authenticate", realm) w.Header().Set("WWW-Authenticate", realm)
ginCtx.AbortWithStatusJSON(http.StatusUnauthorized, NewError(UNAUTHORIZED)) w.Header().Set("Content-Type", "application/json")
WriteJSON(w, http.StatusUnauthorized, NewError(UNAUTHORIZED))
} }
func BasicAuthHandler(c *Controller) gin.HandlerFunc { func BasicAuthHandler(c *Controller) mux.MiddlewareFunc {
if c.Config.HTTP.Auth.HTPasswd.Path == "" { if c.Config.HTTP.Auth.HTPasswd.Path == "" {
// no authentication // no authentication
return func(ginCtx *gin.Context) { return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Process request
next.ServeHTTP(w, r)
})
} }
} }
@ -49,43 +54,48 @@ func BasicAuthHandler(c *Controller) gin.HandlerFunc {
credMap[tokens[0]] = tokens[1] credMap[tokens[0]] = tokens[1]
} }
return func(ginCtx *gin.Context) { return func(next http.Handler) http.Handler {
basicAuth := ginCtx.Request.Header.Get("Authorization") return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if basicAuth == "" { basicAuth := r.Header.Get("Authorization")
authFail(ginCtx, realm, delay) if basicAuth == "" {
return authFail(w, realm, delay)
} return
}
s := strings.SplitN(basicAuth, " ", 2) s := strings.SplitN(basicAuth, " ", 2)
if len(s) != 2 || strings.ToLower(s[0]) != "basic" { if len(s) != 2 || strings.ToLower(s[0]) != "basic" {
authFail(ginCtx, realm, delay) authFail(w, realm, delay)
return return
} }
b, err := base64.StdEncoding.DecodeString(s[1]) b, err := base64.StdEncoding.DecodeString(s[1])
if err != nil { if err != nil {
authFail(ginCtx, realm, delay) authFail(w, realm, delay)
return return
} }
pair := strings.SplitN(string(b), ":", 2) pair := strings.SplitN(string(b), ":", 2)
if len(pair) != 2 { if len(pair) != 2 {
authFail(ginCtx, realm, delay) authFail(w, realm, delay)
return return
} }
username := pair[0] username := pair[0]
passphrase := pair[1] passphrase := pair[1]
passphraseHash, ok := credMap[username] passphraseHash, ok := credMap[username]
if !ok { if !ok {
authFail(ginCtx, realm, delay) authFail(w, realm, delay)
return return
} }
if err := bcrypt.CompareHashAndPassword([]byte(passphraseHash), []byte(passphrase)); err != nil { if err := bcrypt.CompareHashAndPassword([]byte(passphraseHash), []byte(passphrase)); err != nil {
authFail(ginCtx, realm, delay) authFail(w, realm, delay)
return return
} }
// Process request
next.ServeHTTP(w, r)
})
} }
} }

View file

@ -9,13 +9,13 @@ import (
"net/http" "net/http"
"github.com/anuvu/zot/pkg/storage" "github.com/anuvu/zot/pkg/storage"
"github.com/gin-gonic/gin" "github.com/gorilla/mux"
"github.com/rs/zerolog" "github.com/rs/zerolog"
) )
type Controller struct { type Controller struct {
Config *Config Config *Config
Router *gin.Engine Router *mux.Router
ImageStore *storage.ImageStore ImageStore *storage.ImageStore
Log zerolog.Logger Log zerolog.Logger
Server *http.Server Server *http.Server
@ -26,13 +26,8 @@ func NewController(config *Config) *Controller {
} }
func (c *Controller) Run() error { func (c *Controller) Run() error {
if c.Config.Log.Level == "debug" { engine := mux.NewRouter()
gin.SetMode(gin.DebugMode) engine.Use(Logger(c.Log))
} else {
gin.SetMode(gin.ReleaseMode)
}
engine := gin.New()
engine.Use(gin.Recovery(), Logger(c.Log))
c.Router = engine c.Router = engine
_ = NewRouteHandler(c) _ = NewRouteHandler(c)

View file

@ -66,7 +66,7 @@ func TestBasicAuth(t *testing.T) {
}() }()
// without creds, should get access error // without creds, should get access error
resp, err := resty.R().Get(BaseURL1) resp, err := resty.R().Get(BaseURL1 + "/v2/")
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(resp, ShouldNotBeNil) So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 401) So(resp.StatusCode(), ShouldEqual, 401)
@ -135,7 +135,7 @@ func TestTLSWithBasicAuth(t *testing.T) {
So(resp.StatusCode(), ShouldEqual, 400) So(resp.StatusCode(), ShouldEqual, 400)
// without creds, should get access error // without creds, should get access error
resp, err = resty.R().Get(BaseSecureURL2) resp, err = resty.R().Get(BaseSecureURL2 + "/v2/")
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(resp, ShouldNotBeNil) So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 401) So(resp.StatusCode(), ShouldEqual, 401)
@ -220,7 +220,7 @@ func TestTLSMutualAuth(t *testing.T) {
defer func() { resty.SetCertificates(tls.Certificate{}) }() defer func() { resty.SetCertificates(tls.Certificate{}) }()
// with client certs but without creds, should get access error // with client certs but without creds, should get access error
resp, err = resty.R().Get(BaseSecureURL2) resp, err = resty.R().Get(BaseSecureURL2 + "/v2/")
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(resp, ShouldNotBeNil) So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 401) So(resp.StatusCode(), ShouldEqual, 401)

View file

@ -1,10 +1,11 @@
package api package api
import ( import (
"net/http"
"os" "os"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gorilla/mux"
"github.com/rs/zerolog" "github.com/rs/zerolog"
) )
@ -28,43 +29,65 @@ func NewLogger(config *Config) zerolog.Logger {
return log.With().Timestamp().Logger() return log.With().Timestamp().Logger()
} }
func Logger(log zerolog.Logger) gin.HandlerFunc { type statusWriter struct {
http.ResponseWriter
status int
length int
}
func (w *statusWriter) WriteHeader(status int) {
w.status = status
w.ResponseWriter.WriteHeader(status)
}
func (w *statusWriter) Write(b []byte) (int, error) {
if w.status == 0 {
w.status = 200
}
n, err := w.ResponseWriter.Write(b)
w.length += n
return n, err
}
func Logger(log zerolog.Logger) mux.MiddlewareFunc {
l := log.With().Str("module", "http").Logger() l := log.With().Str("module", "http").Logger()
return func(ginCtx *gin.Context) { return func(next http.Handler) http.Handler {
// Start timer return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now() // Start timer
path := ginCtx.Request.URL.Path start := time.Now()
raw := ginCtx.Request.URL.RawQuery path := r.URL.Path
raw := r.URL.RawQuery
// Process request sw := statusWriter{ResponseWriter: w}
ginCtx.Next()
// Stop timer // Process request
end := time.Now() next.ServeHTTP(&sw, r)
latency := end.Sub(start)
if latency > time.Minute {
// Truncate in a golang < 1.8 safe way
latency -= latency % time.Second
}
clientIP := ginCtx.ClientIP()
method := ginCtx.Request.Method
headers := ginCtx.Request.Header
statusCode := ginCtx.Writer.Status()
errMsg := ginCtx.Errors.ByType(gin.ErrorTypePrivate).String()
bodySize := ginCtx.Writer.Size()
if raw != "" {
path = path + "?" + raw
}
l.Info(). // Stop timer
Str("clientIP", clientIP). end := time.Now()
Str("method", method). latency := end.Sub(start)
Str("path", path). if latency > time.Minute {
Int("statusCode", statusCode). // Truncate in a golang < 1.8 safe way
Str("errMsg", errMsg). latency -= latency % time.Second
Str("latency", latency.String()). }
Int("bodySize", bodySize). clientIP := r.RemoteAddr
Interface("headers", headers). method := r.Method
Msg("HTTP API") headers := r.Header
statusCode := sw.status
bodySize := sw.length
if raw != "" {
path = path + "?" + raw
}
l.Info().
Str("clientIP", clientIP).
Str("method", method).
Str("path", path).
Int("statusCode", statusCode).
Str("latency", latency.String()).
Int("bodySize", bodySize).
Interface("headers", headers).
Msg("HTTP API")
})
} }
} }

73
pkg/api/regexp.go Normal file
View file

@ -0,0 +1,73 @@
package api
import "regexp"
// nolint (gochecknoglobals)
var (
// alphaNumericRegexp defines the alpha numeric atom, typically a
// component of names. This only allows lower case characters and digits.
alphaNumericRegexp = match(`[a-z0-9]+`)
// separatorRegexp defines the separators allowed to be embedded in name
// components. This allow one period, one or two underscore and multiple
// dashes.
separatorRegexp = match(`(?:[._]|__|[-]*)`)
// nameComponentRegexp restricts registry path component names to start
// with at least one letter or number, with following parts able to be
// separated by one period, one or two underscore and multiple dashes.
nameComponentRegexp = expression(
alphaNumericRegexp,
optional(repeated(separatorRegexp, alphaNumericRegexp)))
// NameRegexp is the format for the name component of references. The
// regexp has capturing groups for the domain and name part omitting
// the separating forward slash from either.
NameRegexp = expression(
nameComponentRegexp,
optional(repeated(literal(`/`), nameComponentRegexp)))
)
// match compiles the string to a regular expression.
// nolint (gochecknoglobals)
var match = regexp.MustCompile
// literal compiles s into a literal regular expression, escaping any regexp
// reserved characters.
func literal(s string) *regexp.Regexp {
re := match(regexp.QuoteMeta(s))
if _, complete := re.LiteralPrefix(); !complete {
panic("must be a literal")
}
return re
}
// expression defines a full expression, where each regular expression must
// follow the previous.
func expression(res ...*regexp.Regexp) *regexp.Regexp {
var s string
for _, re := range res {
s += re.String()
}
return match(s)
}
// optional wraps the expression in a non-capturing group and makes the
// production optional.
func optional(res ...*regexp.Regexp) *regexp.Regexp {
return match(group(expression(res...)).String() + `?`)
}
// repeated wraps the regexp in a non-capturing group to get one or more
// matches.
func repeated(res ...*regexp.Regexp) *regexp.Regexp {
return match(group(expression(res...)).String() + `+`)
}
// group wraps the regexp in a non-capturing group.
func group(res ...*regexp.Regexp) *regexp.Regexp {
return match(`(?:` + expression(res...).String() + `)`)
}

View file

@ -13,6 +13,8 @@ package api
import ( import (
"fmt" "fmt"
"io"
"io/ioutil"
"net/http" "net/http"
"path" "path"
"strconv" "strconv"
@ -20,10 +22,10 @@ import (
_ "github.com/anuvu/zot/docs" // nolint (golint) - as required by swaggo _ "github.com/anuvu/zot/docs" // nolint (golint) - as required by swaggo
"github.com/anuvu/zot/errors" "github.com/anuvu/zot/errors"
"github.com/gin-gonic/gin" "github.com/gorilla/mux"
jsoniter "github.com/json-iterator/go"
ispec "github.com/opencontainers/image-spec/specs-go/v1" ispec "github.com/opencontainers/image-spec/specs-go/v1"
ginSwagger "github.com/swaggo/gin-swagger" httpSwagger "github.com/swaggo/http-swagger"
"github.com/swaggo/gin-swagger/swaggerFiles"
) )
const RoutePrefix = "/v2" const RoutePrefix = "/v2"
@ -43,36 +45,41 @@ func NewRouteHandler(c *Controller) *RouteHandler {
func (rh *RouteHandler) SetupRoutes() { func (rh *RouteHandler) SetupRoutes() {
rh.c.Router.Use(BasicAuthHandler(rh.c)) rh.c.Router.Use(BasicAuthHandler(rh.c))
g := rh.c.Router.Group(RoutePrefix) g := rh.c.Router.PathPrefix(RoutePrefix).Subrouter()
{ {
g.GET("/", rh.CheckVersionSupport) g.HandleFunc(fmt.Sprintf("/{name:%s}/tags/list", NameRegexp.String()),
g.GET("/:name/tags/list", rh.ListTags) rh.ListTags).Methods("GET")
g.HEAD("/:name/manifests/:reference", rh.CheckManifest) g.HandleFunc(fmt.Sprintf("/{name:%s}/manifests/{reference}", NameRegexp.String()),
g.GET("/:name/manifests/:reference", rh.GetManifest) rh.CheckManifest).Methods("HEAD")
g.PUT("/:name/manifests/:reference", rh.UpdateManifest) g.HandleFunc(fmt.Sprintf("/{name:%s}/manifests/{reference}", NameRegexp.String()),
g.DELETE("/:name/manifests/:reference", rh.DeleteManifest) rh.GetManifest).Methods("GET")
g.HEAD("/:name/blobs/:digest", rh.CheckBlob) g.HandleFunc(fmt.Sprintf("/{name:%s}/manifests/{reference}", NameRegexp.String()),
g.GET("/:name/blobs/:digest", rh.GetBlob) rh.UpdateManifest).Methods("PUT")
g.DELETE("/:name/blobs/:digest", rh.DeleteBlob) g.HandleFunc(fmt.Sprintf("/{name:%s}/manifests/{reference}", NameRegexp.String()),
rh.DeleteManifest).Methods("DELETE")
// NOTE: some routes as per the spec need to be setup with URL params which g.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/{digest}", NameRegexp.String()),
// must equal specific keywords rh.CheckBlob).Methods("HEAD")
g.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/{digest}", NameRegexp.String()),
// route for POST "/v2/:name/blobs/uploads/" and param ":digest"="uploads" rh.GetBlob).Methods("GET")
g.POST("/:name/blobs/:digest/", rh.CreateBlobUpload) g.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/{digest}", NameRegexp.String()),
// route for GET "/v2/:name/blobs/uploads/:uuid" and param ":digest"="uploads" rh.DeleteBlob).Methods("DELETE")
g.GET("/:name/blobs/:digest/:uuid", rh.GetBlobUpload) g.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/uploads/", NameRegexp.String()),
// route for PATCH "/v2/:name/blobs/uploads/:uuid" and param ":digest"="uploads" rh.CreateBlobUpload).Methods("POST")
g.PATCH("/:name/blobs/:digest/:uuid", rh.PatchBlobUpload) g.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/uploads/{uuid}", NameRegexp.String()),
// route for PUT "/v2/:name/blobs/uploads/:uuid" and param ":digest"="uploads" rh.GetBlobUpload).Methods("GET")
g.PUT("/:name/blobs/:digest/:uuid", rh.UpdateBlobUpload) g.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/uploads/{uuid}", NameRegexp.String()),
// route for DELETE "/v2/:name/blobs/uploads/:uuid" and param ":digest"="uploads" rh.PatchBlobUpload).Methods("PATCH")
g.DELETE("/:name/blobs/:digest/:uuid", rh.DeleteBlobUpload) g.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/uploads/{uuid}", NameRegexp.String()),
// route for GET "/v2/_catalog" and param ":name"="_catalog" rh.UpdateBlobUpload).Methods("PUT")
g.GET("/:name", rh.ListRepositories) g.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/uploads/{uuid}", NameRegexp.String()),
rh.DeleteBlobUpload).Methods("DELETE")
g.HandleFunc("/_catalog",
rh.ListRepositories).Methods("GET")
g.HandleFunc("/",
rh.CheckVersionSupport).Methods("GET")
} }
// swagger docs "/swagger/v2/index.html" // swagger docs "/swagger/v2/index.html"
rh.c.Router.GET("/swagger/v2/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) rh.c.Router.PathPrefix("/swagger/v2/").Methods("GET").Handler(httpSwagger.WrapHandler)
} }
// Method handlers // Method handlers
@ -84,9 +91,9 @@ func (rh *RouteHandler) SetupRoutes() {
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Success 200 {string} string "ok" // @Success 200 {string} string "ok"
func (rh *RouteHandler) CheckVersionSupport(ginCtx *gin.Context) { func (rh *RouteHandler) CheckVersionSupport(w http.ResponseWriter, r *http.Request) {
ginCtx.Data(http.StatusOK, "application/json", []byte{}) w.Header().Set(DistAPIVersion, "registry/2.0")
ginCtx.Header(DistAPIVersion, "registry/2.0") WriteData(w, http.StatusOK, "application/json", []byte{})
} }
type ImageTags struct { type ImageTags struct {
@ -103,20 +110,21 @@ type ImageTags struct {
// @Param name path string true "test" // @Param name path string true "test"
// @Success 200 {object} api.ImageTags // @Success 200 {object} api.ImageTags
// @Failure 404 {string} string "not found" // @Failure 404 {string} string "not found"
func (rh *RouteHandler) ListTags(ginCtx *gin.Context) { func (rh *RouteHandler) ListTags(w http.ResponseWriter, r *http.Request) {
name := ginCtx.Param("name") vars := mux.Vars(r)
if name == "" { name, ok := vars["name"]
ginCtx.Status(http.StatusNotFound) if !ok || name == "" {
w.WriteHeader(http.StatusNotFound)
return return
} }
tags, err := rh.c.ImageStore.GetImageTags(name) tags, err := rh.c.ImageStore.GetImageTags(name)
if err != nil { if err != nil {
ginCtx.JSON(http.StatusNotFound, NewError(NAME_UNKNOWN, map[string]string{"name": name})) WriteJSON(w, http.StatusNotFound, NewError(NAME_UNKNOWN, map[string]string{"name": name}))
return return
} }
ginCtx.JSON(http.StatusOK, ImageTags{Name: name, Tags: tags}) WriteJSON(w, http.StatusOK, ImageTags{Name: name, Tags: tags})
} }
// CheckManifest godoc // CheckManifest godoc
@ -131,16 +139,17 @@ func (rh *RouteHandler) ListTags(ginCtx *gin.Context) {
// @Header 200 {object} api.DistContentDigestKey // @Header 200 {object} api.DistContentDigestKey
// @Failure 404 {string} string "not found" // @Failure 404 {string} string "not found"
// @Failure 500 {string} string "internal server error" // @Failure 500 {string} string "internal server error"
func (rh *RouteHandler) CheckManifest(ginCtx *gin.Context) { func (rh *RouteHandler) CheckManifest(w http.ResponseWriter, r *http.Request) {
name := ginCtx.Param("name") vars := mux.Vars(r)
if name == "" { name, ok := vars["name"]
ginCtx.Status(http.StatusNotFound) if !ok || name == "" {
w.WriteHeader(http.StatusNotFound)
return return
} }
reference := ginCtx.Param("reference") reference, ok := vars["reference"]
if reference == "" { if !ok || reference == "" {
ginCtx.JSON(http.StatusNotFound, NewError(MANIFEST_INVALID, map[string]string{"reference": reference})) WriteJSON(w, http.StatusNotFound, NewError(MANIFEST_INVALID, map[string]string{"reference": reference}))
return return
} }
@ -148,16 +157,16 @@ func (rh *RouteHandler) CheckManifest(ginCtx *gin.Context) {
if err != nil { if err != nil {
switch err { switch err {
case errors.ErrManifestNotFound: case errors.ErrManifestNotFound:
ginCtx.JSON(http.StatusNotFound, NewError(MANIFEST_UNKNOWN, map[string]string{"reference": reference})) WriteJSON(w, http.StatusNotFound, NewError(MANIFEST_UNKNOWN, map[string]string{"reference": reference}))
default: default:
ginCtx.JSON(http.StatusInternalServerError, NewError(MANIFEST_INVALID, map[string]string{"reference": reference})) WriteJSON(w, http.StatusInternalServerError, NewError(MANIFEST_INVALID, map[string]string{"reference": reference}))
} }
return return
} }
ginCtx.Status(http.StatusOK) w.Header().Set(DistContentDigestKey, digest)
ginCtx.Header(DistContentDigestKey, digest) w.Header().Set("Content-Length", "0")
ginCtx.Header("Content-Length", "0") w.WriteHeader(http.StatusOK)
} }
// NOTE: https://github.com/swaggo/swag/issues/387 // NOTE: https://github.com/swaggo/swag/issues/387
@ -177,16 +186,17 @@ type ImageManifest struct {
// @Failure 404 {string} string "not found" // @Failure 404 {string} string "not found"
// @Failure 500 {string} string "internal server error" // @Failure 500 {string} string "internal server error"
// @Router /v2/{name}/manifests/{reference} [get] // @Router /v2/{name}/manifests/{reference} [get]
func (rh *RouteHandler) GetManifest(ginCtx *gin.Context) { func (rh *RouteHandler) GetManifest(w http.ResponseWriter, r *http.Request) {
name := ginCtx.Param("name") vars := mux.Vars(r)
if name == "" { name, ok := vars["name"]
ginCtx.Status(http.StatusNotFound) if !ok || name == "" {
w.WriteHeader(http.StatusNotFound)
return return
} }
reference := ginCtx.Param("reference") reference, ok := vars["reference"]
if reference == "" { if !ok || reference == "" {
ginCtx.JSON(http.StatusNotFound, NewError(MANIFEST_UNKNOWN, map[string]string{"reference": reference})) WriteJSON(w, http.StatusNotFound, NewError(MANIFEST_UNKNOWN, map[string]string{"reference": reference}))
return return
} }
@ -194,19 +204,19 @@ func (rh *RouteHandler) GetManifest(ginCtx *gin.Context) {
if err != nil { if err != nil {
switch err { switch err {
case errors.ErrRepoNotFound: case errors.ErrRepoNotFound:
ginCtx.JSON(http.StatusNotFound, NewError(NAME_UNKNOWN, map[string]string{"name": name})) WriteJSON(w, http.StatusNotFound, NewError(NAME_UNKNOWN, map[string]string{"name": name}))
case errors.ErrRepoBadVersion: case errors.ErrRepoBadVersion:
ginCtx.JSON(http.StatusNotFound, NewError(NAME_UNKNOWN, map[string]string{"name": name})) WriteJSON(w, http.StatusNotFound, NewError(NAME_UNKNOWN, map[string]string{"name": name}))
case errors.ErrManifestNotFound: case errors.ErrManifestNotFound:
ginCtx.JSON(http.StatusNotFound, NewError(MANIFEST_UNKNOWN, map[string]string{"reference": reference})) WriteJSON(w, http.StatusNotFound, NewError(MANIFEST_UNKNOWN, map[string]string{"reference": reference}))
default: default:
ginCtx.Status(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
} }
return return
} }
ginCtx.Data(http.StatusOK, mediaType, content) WriteData(w, http.StatusOK, mediaType, content)
ginCtx.Header(DistContentDigestKey, digest) w.Header().Set(DistContentDigestKey, digest)
} }
// UpdateManifest godoc // UpdateManifest godoc
@ -222,28 +232,29 @@ func (rh *RouteHandler) GetManifest(ginCtx *gin.Context) {
// @Failure 404 {string} string "not found" // @Failure 404 {string} string "not found"
// @Failure 500 {string} string "internal server error" // @Failure 500 {string} string "internal server error"
// @Router /v2/{name}/manifests/{reference} [put] // @Router /v2/{name}/manifests/{reference} [put]
func (rh *RouteHandler) UpdateManifest(ginCtx *gin.Context) { func (rh *RouteHandler) UpdateManifest(w http.ResponseWriter, r *http.Request) {
name := ginCtx.Param("name") vars := mux.Vars(r)
if name == "" { name, ok := vars["name"]
ginCtx.Status(http.StatusNotFound) if !ok || name == "" {
w.WriteHeader(http.StatusNotFound)
return return
} }
reference := ginCtx.Param("reference") reference, ok := vars["reference"]
if reference == "" { if !ok || reference == "" {
ginCtx.JSON(http.StatusNotFound, NewError(MANIFEST_INVALID, map[string]string{"reference": reference})) WriteJSON(w, http.StatusNotFound, NewError(MANIFEST_INVALID, map[string]string{"reference": reference}))
return return
} }
mediaType := ginCtx.ContentType() mediaType := r.Header.Get("Content-Type")
if mediaType != ispec.MediaTypeImageManifest { if mediaType != ispec.MediaTypeImageManifest {
ginCtx.Status(http.StatusUnsupportedMediaType) w.WriteHeader(http.StatusUnsupportedMediaType)
return return
} }
body, err := ginCtx.GetRawData() body, err := ioutil.ReadAll(r.Body)
if err != nil { if err != nil {
ginCtx.Status(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
return return
} }
@ -251,22 +262,22 @@ func (rh *RouteHandler) UpdateManifest(ginCtx *gin.Context) {
if err != nil { if err != nil {
switch err { switch err {
case errors.ErrRepoNotFound: case errors.ErrRepoNotFound:
ginCtx.JSON(http.StatusNotFound, NewError(NAME_UNKNOWN, map[string]string{"name": name})) WriteJSON(w, http.StatusNotFound, NewError(NAME_UNKNOWN, map[string]string{"name": name}))
case errors.ErrManifestNotFound: case errors.ErrManifestNotFound:
ginCtx.JSON(http.StatusNotFound, NewError(MANIFEST_UNKNOWN, map[string]string{"reference": reference})) WriteJSON(w, http.StatusNotFound, NewError(MANIFEST_UNKNOWN, map[string]string{"reference": reference}))
case errors.ErrBadManifest: case errors.ErrBadManifest:
ginCtx.JSON(http.StatusBadRequest, NewError(MANIFEST_INVALID, map[string]string{"reference": reference})) WriteJSON(w, http.StatusBadRequest, NewError(MANIFEST_INVALID, map[string]string{"reference": reference}))
case errors.ErrBlobNotFound: case errors.ErrBlobNotFound:
ginCtx.JSON(http.StatusBadRequest, NewError(BLOB_UNKNOWN, map[string]string{"blob": digest})) WriteJSON(w, http.StatusBadRequest, NewError(BLOB_UNKNOWN, map[string]string{"blob": digest}))
default: default:
ginCtx.Status(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
} }
return return
} }
ginCtx.Status(http.StatusCreated) w.Header().Set("Location", fmt.Sprintf("/v2/%s/manifests/%s", name, digest))
ginCtx.Header("Location", fmt.Sprintf("/v2/%s/manifests/%s", name, digest)) w.Header().Set(DistContentDigestKey, digest)
ginCtx.Header(DistContentDigestKey, digest) w.WriteHeader(http.StatusCreated)
} }
// DeleteManifest godoc // DeleteManifest godoc
@ -278,16 +289,17 @@ func (rh *RouteHandler) UpdateManifest(ginCtx *gin.Context) {
// @Param reference path string true "image reference or digest" // @Param reference path string true "image reference or digest"
// @Success 200 {string} string "ok" // @Success 200 {string} string "ok"
// @Router /v2/{name}/manifests/{reference} [delete] // @Router /v2/{name}/manifests/{reference} [delete]
func (rh *RouteHandler) DeleteManifest(ginCtx *gin.Context) { func (rh *RouteHandler) DeleteManifest(w http.ResponseWriter, r *http.Request) {
name := ginCtx.Param("name") vars := mux.Vars(r)
if name == "" { name, ok := vars["name"]
ginCtx.Status(http.StatusNotFound) if !ok || name == "" {
w.WriteHeader(http.StatusNotFound)
return return
} }
reference := ginCtx.Param("reference") reference, ok := vars["reference"]
if reference == "" { if !ok || reference == "" {
ginCtx.Status(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
return return
} }
@ -295,16 +307,16 @@ func (rh *RouteHandler) DeleteManifest(ginCtx *gin.Context) {
if err != nil { if err != nil {
switch err { switch err {
case errors.ErrRepoNotFound: case errors.ErrRepoNotFound:
ginCtx.JSON(http.StatusNotFound, NewError(NAME_UNKNOWN, map[string]string{"name": name})) WriteJSON(w, http.StatusNotFound, NewError(NAME_UNKNOWN, map[string]string{"name": name}))
case errors.ErrManifestNotFound: case errors.ErrManifestNotFound:
ginCtx.JSON(http.StatusNotFound, NewError(MANIFEST_UNKNOWN, map[string]string{"reference": reference})) WriteJSON(w, http.StatusNotFound, NewError(MANIFEST_UNKNOWN, map[string]string{"reference": reference}))
default: default:
ginCtx.Status(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
} }
return return
} }
ginCtx.Status(http.StatusOK) w.WriteHeader(http.StatusOK)
} }
// CheckBlob godoc // CheckBlob godoc
@ -317,44 +329,45 @@ func (rh *RouteHandler) DeleteManifest(ginCtx *gin.Context) {
// @Success 200 {object} api.ImageManifest // @Success 200 {object} api.ImageManifest
// @Header 200 {object} api.DistContentDigestKey // @Header 200 {object} api.DistContentDigestKey
// @Router /v2/{name}/blobs/{digest} [head] // @Router /v2/{name}/blobs/{digest} [head]
func (rh *RouteHandler) CheckBlob(ginCtx *gin.Context) { func (rh *RouteHandler) CheckBlob(w http.ResponseWriter, r *http.Request) {
name := ginCtx.Param("name") vars := mux.Vars(r)
if name == "" { name, ok := vars["name"]
ginCtx.Status(http.StatusNotFound) if !ok || name == "" {
w.WriteHeader(http.StatusNotFound)
return return
} }
digest := ginCtx.Param("digest") digest, ok := vars["digest"]
if digest == "" { if !ok || digest == "" {
ginCtx.Status(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
return return
} }
mediaType := ginCtx.Request.Header.Get("Accept") mediaType := r.Header.Get("Accept")
ok, blen, err := rh.c.ImageStore.CheckBlob(name, digest, mediaType) ok, blen, err := rh.c.ImageStore.CheckBlob(name, digest, mediaType)
if err != nil { if err != nil {
switch err { switch err {
case errors.ErrBadBlobDigest: case errors.ErrBadBlobDigest:
ginCtx.JSON(http.StatusBadRequest, NewError(DIGEST_INVALID, map[string]string{"digest": digest})) WriteJSON(w, http.StatusBadRequest, NewError(DIGEST_INVALID, map[string]string{"digest": digest}))
case errors.ErrRepoNotFound: case errors.ErrRepoNotFound:
ginCtx.JSON(http.StatusNotFound, NewError(NAME_UNKNOWN, map[string]string{"name": name})) WriteJSON(w, http.StatusNotFound, NewError(NAME_UNKNOWN, map[string]string{"name": name}))
case errors.ErrBlobNotFound: case errors.ErrBlobNotFound:
ginCtx.JSON(http.StatusNotFound, NewError(BLOB_UNKNOWN, map[string]string{"digest": digest})) WriteJSON(w, http.StatusNotFound, NewError(BLOB_UNKNOWN, map[string]string{"digest": digest}))
default: default:
ginCtx.Status(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
} }
return return
} }
if !ok { if !ok {
ginCtx.JSON(http.StatusNotFound, NewError(BLOB_UNKNOWN, map[string]string{"digest": digest})) WriteJSON(w, http.StatusNotFound, NewError(BLOB_UNKNOWN, map[string]string{"digest": digest}))
return return
} }
ginCtx.Status(http.StatusOK) w.Header().Set("Content-Length", fmt.Sprintf("%d", blen))
ginCtx.Header("Content-Length", fmt.Sprintf("%d", blen)) w.Header().Set(DistContentDigestKey, digest)
ginCtx.Header(DistContentDigestKey, digest) w.WriteHeader(http.StatusOK)
} }
// GetBlob godoc // GetBlob godoc
@ -367,41 +380,41 @@ func (rh *RouteHandler) CheckBlob(ginCtx *gin.Context) {
// @Header 200 {object} api.DistContentDigestKey // @Header 200 {object} api.DistContentDigestKey
// @Success 200 {object} api.ImageManifest // @Success 200 {object} api.ImageManifest
// @Router /v2/{name}/blobs/{digest} [get] // @Router /v2/{name}/blobs/{digest} [get]
func (rh *RouteHandler) GetBlob(ginCtx *gin.Context) { func (rh *RouteHandler) GetBlob(w http.ResponseWriter, r *http.Request) {
name := ginCtx.Param("name") vars := mux.Vars(r)
if name == "" { name, ok := vars["name"]
ginCtx.Status(http.StatusNotFound) if !ok || name == "" {
w.WriteHeader(http.StatusNotFound)
return return
} }
digest := ginCtx.Param("digest") digest, ok := vars["digest"]
if digest == "" { if !ok || digest == "" {
ginCtx.Status(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
return return
} }
mediaType := ginCtx.Request.Header.Get("Accept") mediaType := r.Header.Get("Accept")
br, blen, err := rh.c.ImageStore.GetBlob(name, digest, mediaType) br, blen, err := rh.c.ImageStore.GetBlob(name, digest, mediaType)
if err != nil { if err != nil {
switch err { switch err {
case errors.ErrBadBlobDigest: case errors.ErrBadBlobDigest:
ginCtx.JSON(http.StatusBadRequest, NewError(DIGEST_INVALID, map[string]string{"digest": digest})) WriteJSON(w, http.StatusBadRequest, NewError(DIGEST_INVALID, map[string]string{"digest": digest}))
case errors.ErrRepoNotFound: case errors.ErrRepoNotFound:
ginCtx.JSON(http.StatusNotFound, NewError(NAME_UNKNOWN, map[string]string{"name": name})) WriteJSON(w, http.StatusNotFound, NewError(NAME_UNKNOWN, map[string]string{"name": name}))
case errors.ErrBlobNotFound: case errors.ErrBlobNotFound:
ginCtx.JSON(http.StatusNotFound, NewError(BLOB_UNKNOWN, map[string]string{"digest": digest})) WriteJSON(w, http.StatusNotFound, NewError(BLOB_UNKNOWN, map[string]string{"digest": digest}))
default: default:
ginCtx.Status(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
} }
return return
} }
ginCtx.Status(http.StatusOK) w.Header().Set("Content-Length", fmt.Sprintf("%d", blen))
ginCtx.Header("Content-Length", fmt.Sprintf("%d", blen)) w.Header().Set(DistContentDigestKey, digest)
ginCtx.Header(DistContentDigestKey, digest)
// return the blob data // return the blob data
ginCtx.DataFromReader(http.StatusOK, blen, mediaType, br, map[string]string{}) WriteDataFromReader(w, http.StatusOK, blen, mediaType, br)
} }
// DeleteBlob godoc // DeleteBlob godoc
@ -413,16 +426,17 @@ func (rh *RouteHandler) GetBlob(ginCtx *gin.Context) {
// @Param digest path string true "blob/layer digest" // @Param digest path string true "blob/layer digest"
// @Success 202 {string} string "accepted" // @Success 202 {string} string "accepted"
// @Router /v2/{name}/blobs/{digest} [delete] // @Router /v2/{name}/blobs/{digest} [delete]
func (rh *RouteHandler) DeleteBlob(ginCtx *gin.Context) { func (rh *RouteHandler) DeleteBlob(w http.ResponseWriter, r *http.Request) {
name := ginCtx.Param("name") vars := mux.Vars(r)
if name == "" { name, ok := vars["name"]
ginCtx.Status(http.StatusNotFound) if !ok || name == "" {
w.WriteHeader(http.StatusNotFound)
return return
} }
digest := ginCtx.Param("digest") digest, ok := vars["digest"]
if digest == "" { if !ok || digest == "" {
ginCtx.Status(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
return return
} }
@ -430,18 +444,18 @@ func (rh *RouteHandler) DeleteBlob(ginCtx *gin.Context) {
if err != nil { if err != nil {
switch err { switch err {
case errors.ErrBadBlobDigest: case errors.ErrBadBlobDigest:
ginCtx.JSON(http.StatusBadRequest, NewError(DIGEST_INVALID, map[string]string{"digest": digest})) WriteJSON(w, http.StatusBadRequest, NewError(DIGEST_INVALID, map[string]string{"digest": digest}))
case errors.ErrRepoNotFound: case errors.ErrRepoNotFound:
ginCtx.JSON(http.StatusNotFound, NewError(NAME_UNKNOWN, map[string]string{"name": name})) WriteJSON(w, http.StatusNotFound, NewError(NAME_UNKNOWN, map[string]string{"name": name}))
case errors.ErrBlobNotFound: case errors.ErrBlobNotFound:
ginCtx.JSON(http.StatusNotFound, NewError(BLOB_UNKNOWN, map[string]string{"digest": digest})) WriteJSON(w, http.StatusNotFound, NewError(BLOB_UNKNOWN, map[string]string{"digest": digest}))
default: default:
ginCtx.Status(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
} }
return return
} }
ginCtx.Status(http.StatusAccepted) w.WriteHeader(http.StatusAccepted)
} }
// CreateBlobUpload godoc // CreateBlobUpload godoc
@ -456,15 +470,11 @@ func (rh *RouteHandler) DeleteBlob(ginCtx *gin.Context) {
// @Failure 404 {string} string "not found" // @Failure 404 {string} string "not found"
// @Failure 500 {string} string "internal server error" // @Failure 500 {string} string "internal server error"
// @Router /v2/{name}/blobs/uploads [post] // @Router /v2/{name}/blobs/uploads [post]
func (rh *RouteHandler) CreateBlobUpload(ginCtx *gin.Context) { func (rh *RouteHandler) CreateBlobUpload(w http.ResponseWriter, r *http.Request) {
if paramIsNot(ginCtx, "digest", "uploads") { vars := mux.Vars(r)
ginCtx.Status(http.StatusNotFound) name, ok := vars["name"]
return if !ok || name == "" {
} w.WriteHeader(http.StatusNotFound)
name := ginCtx.Param("name")
if name == "" {
ginCtx.Status(http.StatusNotFound)
return return
} }
@ -472,16 +482,16 @@ func (rh *RouteHandler) CreateBlobUpload(ginCtx *gin.Context) {
if err != nil { if err != nil {
switch err { switch err {
case errors.ErrRepoNotFound: case errors.ErrRepoNotFound:
ginCtx.JSON(http.StatusNotFound, NewError(NAME_UNKNOWN, map[string]string{"name": name})) WriteJSON(w, http.StatusNotFound, NewError(NAME_UNKNOWN, map[string]string{"name": name}))
default: default:
ginCtx.Status(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
} }
return return
} }
ginCtx.Status(http.StatusAccepted) w.Header().Set("Location", path.Join(r.URL.String(), u))
ginCtx.Header("Location", path.Join(ginCtx.Request.URL.String(), u)) w.Header().Set("Range", "bytes=0-0")
ginCtx.Header("Range", "bytes=0-0") w.WriteHeader(http.StatusAccepted)
} }
// GetBlobUpload godoc // GetBlobUpload godoc
@ -497,21 +507,17 @@ func (rh *RouteHandler) CreateBlobUpload(ginCtx *gin.Context) {
// @Failure 404 {string} string "not found" // @Failure 404 {string} string "not found"
// @Failure 500 {string} string "internal server error" // @Failure 500 {string} string "internal server error"
// @Router /v2/{name}/blobs/uploads/{uuid} [get] // @Router /v2/{name}/blobs/uploads/{uuid} [get]
func (rh *RouteHandler) GetBlobUpload(ginCtx *gin.Context) { func (rh *RouteHandler) GetBlobUpload(w http.ResponseWriter, r *http.Request) {
if paramIsNot(ginCtx, "digest", "uploads") { vars := mux.Vars(r)
ginCtx.Status(http.StatusNotFound) name, ok := vars["name"]
if !ok || name == "" {
w.WriteHeader(http.StatusNotFound)
return return
} }
name := ginCtx.Param("name") uuid, ok := vars["uuid"]
if name == "" { if !ok || uuid == "" {
ginCtx.Status(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
return
}
uuid := ginCtx.Param("uuid")
if uuid == "" {
ginCtx.Status(http.StatusNotFound)
return return
} }
@ -519,22 +525,22 @@ func (rh *RouteHandler) GetBlobUpload(ginCtx *gin.Context) {
if err != nil { if err != nil {
switch err { switch err {
case errors.ErrBadUploadRange: case errors.ErrBadUploadRange:
ginCtx.JSON(http.StatusBadRequest, NewError(BLOB_UPLOAD_INVALID, map[string]string{"uuid": uuid})) WriteJSON(w, http.StatusBadRequest, NewError(BLOB_UPLOAD_INVALID, map[string]string{"uuid": uuid}))
case errors.ErrBadBlobDigest: case errors.ErrBadBlobDigest:
ginCtx.JSON(http.StatusBadRequest, NewError(BLOB_UPLOAD_INVALID, map[string]string{"uuid": uuid})) WriteJSON(w, http.StatusBadRequest, NewError(BLOB_UPLOAD_INVALID, map[string]string{"uuid": uuid}))
case errors.ErrRepoNotFound: case errors.ErrRepoNotFound:
ginCtx.JSON(http.StatusNotFound, NewError(NAME_UNKNOWN, map[string]string{"name": name})) WriteJSON(w, http.StatusNotFound, NewError(NAME_UNKNOWN, map[string]string{"name": name}))
case errors.ErrUploadNotFound: case errors.ErrUploadNotFound:
ginCtx.JSON(http.StatusNotFound, NewError(BLOB_UPLOAD_UNKNOWN, map[string]string{"uuid": uuid})) WriteJSON(w, http.StatusNotFound, NewError(BLOB_UPLOAD_UNKNOWN, map[string]string{"uuid": uuid}))
default: default:
ginCtx.Status(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
} }
return return
} }
ginCtx.Status(http.StatusNoContent) w.Header().Set("Location", path.Join(r.URL.String(), uuid))
ginCtx.Header("Location", path.Join(ginCtx.Request.URL.String(), uuid)) w.Header().Set("Range", fmt.Sprintf("bytes=0-%d", size))
ginCtx.Header("Range", fmt.Sprintf("bytes=0-%d", size)) w.WriteHeader(http.StatusNoContent)
} }
// PatchBlobUpload godoc // PatchBlobUpload godoc
@ -553,72 +559,67 @@ func (rh *RouteHandler) GetBlobUpload(ginCtx *gin.Context) {
// @Failure 416 {string} string "range not satisfiable" // @Failure 416 {string} string "range not satisfiable"
// @Failure 500 {string} string "internal server error" // @Failure 500 {string} string "internal server error"
// @Router /v2/{name}/blobs/uploads/{uuid} [patch] // @Router /v2/{name}/blobs/uploads/{uuid} [patch]
func (rh *RouteHandler) PatchBlobUpload(ginCtx *gin.Context) { func (rh *RouteHandler) PatchBlobUpload(w http.ResponseWriter, r *http.Request) {
rh.c.Log.Info().Interface("headers", r.Header).Msg("request headers")
rh.c.Log.Info().Interface("headers", ginCtx.Request.Header).Msg("request headers") vars := mux.Vars(r)
if paramIsNot(ginCtx, "digest", "uploads") { name, ok := vars["name"]
ginCtx.Status(http.StatusNotFound) if !ok || name == "" {
w.WriteHeader(http.StatusNotFound)
return return
} }
uuid, ok := vars["uuid"]
name := ginCtx.Param("name") if !ok || uuid == "" {
if name == "" { w.WriteHeader(http.StatusNotFound)
ginCtx.Status(http.StatusNotFound)
return
}
uuid := ginCtx.Param("uuid")
if uuid == "" {
ginCtx.Status(http.StatusNotFound)
return return
} }
var err error var err error
var contentLength int64 var contentLength int64
if contentLength, err = strconv.ParseInt(ginCtx.Request.Header.Get("Content-Length"), 10, 64); err != nil { if contentLength, err = strconv.ParseInt(r.Header.Get("Content-Length"), 10, 64); err != nil {
rh.c.Log.Warn().Str("actual", ginCtx.Request.Header.Get("Content-Length")).Msg("invalid content length") rh.c.Log.Warn().Str("actual", r.Header.Get("Content-Length")).Msg("invalid content length")
ginCtx.Status(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
return return
} }
contentRange := ginCtx.Request.Header.Get("Content-Range") contentRange := r.Header.Get("Content-Range")
if contentRange == "" { if contentRange == "" {
rh.c.Log.Warn().Str("actual", ginCtx.Request.Header.Get("Content-Range")).Msg("invalid content range") rh.c.Log.Warn().Str("actual", r.Header.Get("Content-Range")).Msg("invalid content range")
ginCtx.Status(http.StatusRequestedRangeNotSatisfiable) w.WriteHeader(http.StatusRequestedRangeNotSatisfiable)
return return
} }
var from, to int64 var from, to int64
if from, to, err = getContentRange(ginCtx); err != nil || (to-from) != contentLength { if from, to, err = getContentRange(r); err != nil || (to-from) != contentLength {
ginCtx.Status(http.StatusRequestedRangeNotSatisfiable) w.WriteHeader(http.StatusRequestedRangeNotSatisfiable)
return return
} }
if ginCtx.ContentType() != "application/octet-stream" { if contentType := r.Header.Get("Content-Type"); contentType != "application/octet-stream" {
rh.c.Log.Warn().Str("actual", ginCtx.ContentType()).Msg("invalid media type") rh.c.Log.Warn().Str("actual", contentType).Str("expected", "application/octet-stream").Msg("invalid media type")
ginCtx.Status(http.StatusUnsupportedMediaType) w.WriteHeader(http.StatusUnsupportedMediaType)
return return
} }
clen, err := rh.c.ImageStore.PutBlobChunk(name, uuid, from, to, ginCtx.Request.Body) clen, err := rh.c.ImageStore.PutBlobChunk(name, uuid, from, to, r.Body)
if err != nil { if err != nil {
switch err { switch err {
case errors.ErrBadUploadRange: case errors.ErrBadUploadRange:
ginCtx.JSON(http.StatusBadRequest, NewError(BLOB_UPLOAD_INVALID, map[string]string{"uuid": uuid})) WriteJSON(w, http.StatusBadRequest, NewError(BLOB_UPLOAD_INVALID, map[string]string{"uuid": uuid}))
case errors.ErrRepoNotFound: case errors.ErrRepoNotFound:
ginCtx.JSON(http.StatusNotFound, NewError(NAME_UNKNOWN, map[string]string{"name": name})) WriteJSON(w, http.StatusNotFound, NewError(NAME_UNKNOWN, map[string]string{"name": name}))
case errors.ErrUploadNotFound: case errors.ErrUploadNotFound:
ginCtx.JSON(http.StatusNotFound, NewError(BLOB_UPLOAD_UNKNOWN, map[string]string{"uuid": uuid})) WriteJSON(w, http.StatusNotFound, NewError(BLOB_UPLOAD_UNKNOWN, map[string]string{"uuid": uuid}))
default: default:
ginCtx.Status(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
} }
return return
} }
ginCtx.Status(http.StatusAccepted) w.Header().Set("Location", path.Join(r.URL.String(), uuid))
ginCtx.Header("Location", path.Join(ginCtx.Request.URL.String(), uuid)) w.Header().Set("Range", fmt.Sprintf("bytes=0-%d", clen))
ginCtx.Header("Range", fmt.Sprintf("bytes=0-%d", clen)) w.Header().Set("Content-Length", "0")
ginCtx.Header("Content-Length", "0") w.Header().Set(BlobUploadUUID, uuid)
ginCtx.Header(BlobUploadUUID, uuid) w.WriteHeader(http.StatusAccepted)
} }
// UpdateBlobUpload godoc // UpdateBlobUpload godoc
@ -635,105 +636,102 @@ func (rh *RouteHandler) PatchBlobUpload(ginCtx *gin.Context) {
// @Failure 404 {string} string "not found" // @Failure 404 {string} string "not found"
// @Failure 500 {string} string "internal server error" // @Failure 500 {string} string "internal server error"
// @Router /v2/{name}/blobs/uploads/{uuid} [put] // @Router /v2/{name}/blobs/uploads/{uuid} [put]
func (rh *RouteHandler) UpdateBlobUpload(ginCtx *gin.Context) { func (rh *RouteHandler) UpdateBlobUpload(w http.ResponseWriter, r *http.Request) {
if paramIsNot(ginCtx, "digest", "uploads") { vars := mux.Vars(r)
ginCtx.Status(http.StatusNotFound) name, ok := vars["name"]
if !ok || name == "" {
w.WriteHeader(http.StatusNotFound)
return return
} }
name := ginCtx.Param("name") uuid, ok := vars["uuid"]
if name == "" { if !ok || uuid == "" {
ginCtx.Status(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
return return
} }
uuid := ginCtx.Param("uuid") digests, ok := r.URL.Query()["digest"]
if uuid == "" { if !ok || len(digests) != 1 {
ginCtx.Status(http.StatusNotFound) w.WriteHeader(http.StatusBadRequest)
return
}
digest := ginCtx.Query("digest")
if digest == "" {
ginCtx.Status(http.StatusBadRequest)
return return
} }
digest := digests[0]
contentPresent := true contentPresent := true
contentLen, err := strconv.ParseInt(ginCtx.Request.Header.Get("Content-Length"), 10, 64) contentLen, err := strconv.ParseInt(r.Header.Get("Content-Length"), 10, 64)
if err != nil || contentLen == 0 { if err != nil || contentLen == 0 {
contentPresent = false contentPresent = false
} }
contentRangePresent := true contentRangePresent := true
if ginCtx.Request.Header.Get("Content-Range") == "" { if r.Header.Get("Content-Range") == "" {
contentRangePresent = false contentRangePresent = false
} }
// we expect at least one of "Content-Length" or "Content-Range" to be // we expect at least one of "Content-Length" or "Content-Range" to be
// present // present
if !contentPresent && !contentRangePresent { if !contentPresent && !contentRangePresent {
ginCtx.Status(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
return return
} }
var from, to int64 var from, to int64
if contentPresent { if contentPresent {
if ginCtx.ContentType() != "application/octet-stream" { if r.Header.Get("Content-Type") != "application/octet-stream" {
ginCtx.Status(http.StatusUnsupportedMediaType) w.WriteHeader(http.StatusUnsupportedMediaType)
return return
} }
contentRange := ginCtx.Request.Header.Get("Content-Range") contentRange := r.Header.Get("Content-Range")
if contentRange == "" { // monolithic upload if contentRange == "" { // monolithic upload
from = 0 from = 0
if contentLen == 0 { if contentLen == 0 {
ginCtx.Status(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
return return
} }
to = contentLen to = contentLen
} else if from, to, err = getContentRange(ginCtx); err != nil { // finish chunked upload } else if from, to, err = getContentRange(r); err != nil { // finish chunked upload
ginCtx.Status(http.StatusRequestedRangeNotSatisfiable) w.WriteHeader(http.StatusRequestedRangeNotSatisfiable)
return return
} }
_, err = rh.c.ImageStore.PutBlobChunk(name, uuid, from, to, ginCtx.Request.Body) _, err = rh.c.ImageStore.PutBlobChunk(name, uuid, from, to, r.Body)
if err != nil { if err != nil {
switch err { switch err {
case errors.ErrBadUploadRange: case errors.ErrBadUploadRange:
ginCtx.JSON(http.StatusBadRequest, NewError(BLOB_UPLOAD_INVALID, map[string]string{"uuid": uuid})) WriteJSON(w, http.StatusBadRequest, NewError(BLOB_UPLOAD_INVALID, map[string]string{"uuid": uuid}))
case errors.ErrRepoNotFound: case errors.ErrRepoNotFound:
ginCtx.JSON(http.StatusNotFound, NewError(NAME_UNKNOWN, map[string]string{"name": name})) WriteJSON(w, http.StatusNotFound, NewError(NAME_UNKNOWN, map[string]string{"name": name}))
case errors.ErrUploadNotFound: case errors.ErrUploadNotFound:
ginCtx.JSON(http.StatusNotFound, NewError(BLOB_UPLOAD_UNKNOWN, map[string]string{"uuid": uuid})) WriteJSON(w, http.StatusNotFound, NewError(BLOB_UPLOAD_UNKNOWN, map[string]string{"uuid": uuid}))
default: default:
ginCtx.Status(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
} }
return return
} }
} }
// blob chunks already transferred, just finish // blob chunks already transferred, just finish
if err := rh.c.ImageStore.FinishBlobUpload(name, uuid, ginCtx.Request.Body, digest); err != nil { if err := rh.c.ImageStore.FinishBlobUpload(name, uuid, r.Body, digest); err != nil {
switch err { switch err {
case errors.ErrBadBlobDigest: case errors.ErrBadBlobDigest:
ginCtx.JSON(http.StatusBadRequest, NewError(DIGEST_INVALID, map[string]string{"digest": digest})) WriteJSON(w, http.StatusBadRequest, NewError(DIGEST_INVALID, map[string]string{"digest": digest}))
case errors.ErrBadUploadRange: case errors.ErrBadUploadRange:
ginCtx.JSON(http.StatusBadRequest, NewError(BLOB_UPLOAD_INVALID, map[string]string{"uuid": uuid})) WriteJSON(w, http.StatusBadRequest, NewError(BLOB_UPLOAD_INVALID, map[string]string{"uuid": uuid}))
case errors.ErrRepoNotFound: case errors.ErrRepoNotFound:
ginCtx.JSON(http.StatusNotFound, NewError(NAME_UNKNOWN, map[string]string{"name": name})) WriteJSON(w, http.StatusNotFound, NewError(NAME_UNKNOWN, map[string]string{"name": name}))
case errors.ErrUploadNotFound: case errors.ErrUploadNotFound:
ginCtx.JSON(http.StatusNotFound, NewError(BLOB_UPLOAD_UNKNOWN, map[string]string{"uuid": uuid})) WriteJSON(w, http.StatusNotFound, NewError(BLOB_UPLOAD_UNKNOWN, map[string]string{"uuid": uuid}))
default: default:
ginCtx.Status(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
} }
return return
} }
ginCtx.Status(http.StatusCreated) w.Header().Set("Location", fmt.Sprintf("/v2/%s/blobs/%s", name, digest))
ginCtx.Header("Location", fmt.Sprintf("/v2/%s/blobs/%s", name, digest)) w.Header().Set("Content-Length", "0")
ginCtx.Header("Content-Length", "0") w.Header().Set(DistContentDigestKey, digest)
ginCtx.Header(DistContentDigestKey, digest) w.WriteHeader(http.StatusCreated)
} }
// DeleteBlobUpload godoc // DeleteBlobUpload godoc
@ -747,28 +745,33 @@ func (rh *RouteHandler) UpdateBlobUpload(ginCtx *gin.Context) {
// @Failure 404 {string} string "not found" // @Failure 404 {string} string "not found"
// @Failure 500 {string} string "internal server error" // @Failure 500 {string} string "internal server error"
// @Router /v2/{name}/blobs/uploads/{uuid} [delete] // @Router /v2/{name}/blobs/uploads/{uuid} [delete]
func (rh *RouteHandler) DeleteBlobUpload(ginCtx *gin.Context) { func (rh *RouteHandler) DeleteBlobUpload(w http.ResponseWriter, r *http.Request) {
if paramIsNot(ginCtx, "digest", "uploads") { vars := mux.Vars(r)
ginCtx.Status(http.StatusNotFound) name, ok := vars["name"]
if !ok || name == "" {
w.WriteHeader(http.StatusNotFound)
return return
} }
name := ginCtx.Param("name") uuid, ok := vars["uuid"]
uuid := ginCtx.Param("uuid") if !ok || uuid == "" {
w.WriteHeader(http.StatusNotFound)
return
}
if err := rh.c.ImageStore.DeleteBlobUpload(name, uuid); err != nil { if err := rh.c.ImageStore.DeleteBlobUpload(name, uuid); err != nil {
switch err { switch err {
case errors.ErrRepoNotFound: case errors.ErrRepoNotFound:
ginCtx.JSON(http.StatusNotFound, NewError(NAME_UNKNOWN, map[string]string{"name": name})) WriteJSON(w, http.StatusNotFound, NewError(NAME_UNKNOWN, map[string]string{"name": name}))
case errors.ErrUploadNotFound: case errors.ErrUploadNotFound:
ginCtx.JSON(http.StatusNotFound, NewError(BLOB_UPLOAD_UNKNOWN, map[string]string{"uuid": uuid})) WriteJSON(w, http.StatusNotFound, NewError(BLOB_UPLOAD_UNKNOWN, map[string]string{"uuid": uuid}))
default: default:
ginCtx.Status(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
} }
return return
} }
ginCtx.Status(http.StatusOK) w.WriteHeader(http.StatusOK)
} }
type RepositoryList struct { type RepositoryList struct {
@ -783,32 +786,22 @@ type RepositoryList struct {
// @Success 200 {object} api.RepositoryList // @Success 200 {object} api.RepositoryList
// @Failure 500 {string} string "internal server error" // @Failure 500 {string} string "internal server error"
// @Router /v2/_catalog [get] // @Router /v2/_catalog [get]
func (rh *RouteHandler) ListRepositories(ginCtx *gin.Context) { func (rh *RouteHandler) ListRepositories(w http.ResponseWriter, r *http.Request) {
if paramIsNot(ginCtx, "name", "_catalog") {
ginCtx.Status(http.StatusNotFound)
return
}
repos, err := rh.c.ImageStore.GetRepositories() repos, err := rh.c.ImageStore.GetRepositories()
if err != nil { if err != nil {
ginCtx.Status(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
return return
} }
is := RepositoryList{Repositories: repos} is := RepositoryList{Repositories: repos}
ginCtx.JSON(http.StatusOK, is) WriteJSON(w, http.StatusOK, is)
} }
// helper routines // helper routines
func paramIsNot(ginCtx *gin.Context, name string, expected string) bool { func getContentRange(r *http.Request) (int64 /* from */, int64 /* to */, error) {
actual := ginCtx.Param(name) contentRange := r.Header.Get("Content-Range")
return actual != expected
}
func getContentRange(ginCtx *gin.Context) (int64 /* from */, int64 /* to */, error) {
contentRange := ginCtx.Request.Header.Get("Content-Range")
tokens := strings.Split(contentRange, "-") tokens := strings.Split(contentRange, "-")
from, err := strconv.ParseInt(tokens[0], 10, 64) from, err := strconv.ParseInt(tokens[0], 10, 64)
if err != nil { if err != nil {
@ -823,3 +816,36 @@ func getContentRange(ginCtx *gin.Context) (int64 /* from */, int64 /* to */, err
} }
return from, to, nil return from, to, nil
} }
func WriteJSON(w http.ResponseWriter, status int, data interface{}) {
var json = jsoniter.ConfigCompatibleWithStandardLibrary
body, err := json.Marshal(data)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
WriteData(w, status, "application/json; charset=utf-8", body)
}
func WriteData(w http.ResponseWriter, status int, mediaType string, data []byte) {
w.Header().Set("Content-Type", mediaType)
w.WriteHeader(status)
_, _ = w.Write(data)
}
func WriteDataFromReader(w http.ResponseWriter, status int, length int64, mediaType string, reader io.Reader) {
w.Header().Set("Content-Type", mediaType)
w.Header().Set("Content-Length", strconv.FormatInt(length, 10))
const maxSize = 10 * 1024 * 1024
for {
size, err := io.CopyN(w, reader, maxSize)
if size == 0 {
if err != io.EOF {
w.WriteHeader(http.StatusInternalServerError)
return
}
break
}
}
w.WriteHeader(status)
}

View file

@ -1,3 +1,4 @@
// nolint (dupl)
package api_test package api_test
import ( import (
@ -49,7 +50,7 @@ func TestAPI(t *testing.T) {
So(resp.StatusCode(), ShouldEqual, 404) So(resp.StatusCode(), ShouldEqual, 404)
So(resp.String(), ShouldNotBeEmpty) So(resp.String(), ShouldNotBeEmpty)
// after newly created upload should fail // after newly created upload should succeed
resp, err = resty.R().Post(BaseURL + "/v2/repo/blobs/uploads/") resp, err = resty.R().Post(BaseURL + "/v2/repo/blobs/uploads/")
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 202) So(resp.StatusCode(), ShouldEqual, 202)
@ -111,6 +112,57 @@ func TestAPI(t *testing.T) {
So(resp.StatusCode(), ShouldEqual, 200) So(resp.StatusCode(), ShouldEqual, 200)
}) })
Convey("Monolithic blob upload with multiple name components", func() {
resp, err := resty.R().Post(BaseURL + "/v2/repo1/repo2/repo3/blobs/uploads/")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 202)
loc := resp.Header().Get("Location")
So(loc, ShouldNotBeEmpty)
resp, err = resty.R().Get(BaseURL + loc)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 204)
resp, err = resty.R().Get(BaseURL + "/v2/repo1/repo2/repo3/tags/list")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
So(resp.String(), ShouldNotBeEmpty)
// without a "?digest=<>" should fail
content := []byte("this is a blob")
digest := godigest.FromBytes(content)
So(digest, ShouldNotBeNil)
resp, err = resty.R().Put(BaseURL + loc)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 400)
// without the Content-Length should fail
resp, err = resty.R().SetQueryParam("digest", digest.String()).Put(BaseURL + loc)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 400)
// without any data to send, should fail
resp, err = resty.R().SetQueryParam("digest", digest.String()).
SetHeader("Content-Type", "application/octet-stream").Put(BaseURL + loc)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 400)
// monolithic blob upload: success
resp, err = resty.R().SetQueryParam("digest", digest.String()).
SetHeader("Content-Type", "application/octet-stream").SetBody(content).Put(BaseURL + loc)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 201)
blobLoc := resp.Header().Get("Location")
So(blobLoc, ShouldNotBeEmpty)
So(resp.Header().Get("Content-Length"), ShouldEqual, "0")
So(resp.Header().Get(api.DistContentDigestKey), ShouldNotBeEmpty)
// upload reference should now be removed
resp, err = resty.R().Get(BaseURL + loc)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 404)
// blob reference should be accessible
resp, err = resty.R().Get(BaseURL + blobLoc)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
})
Convey("Chunked blob upload", func() { Convey("Chunked blob upload", func() {
resp, err := resty.R().Post(BaseURL + "/v2/repo/blobs/uploads/") resp, err := resty.R().Post(BaseURL + "/v2/repo/blobs/uploads/")
So(err, ShouldBeNil) So(err, ShouldBeNil)
@ -178,6 +230,73 @@ func TestAPI(t *testing.T) {
So(resp.StatusCode(), ShouldEqual, 200) So(resp.StatusCode(), ShouldEqual, 200)
}) })
Convey("Chunked blob upload with multiple name components", func() {
resp, err := resty.R().Post(BaseURL + "/v2/repo4/repo5/repo6/blobs/uploads/")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 202)
loc := resp.Header().Get("Location")
So(loc, ShouldNotBeEmpty)
var buf bytes.Buffer
chunk1 := []byte("this is the first chunk")
n, err := buf.Write(chunk1)
So(n, ShouldEqual, len(chunk1))
So(err, ShouldBeNil)
// write first chunk
contentRange := fmt.Sprintf("%d-%d", 0, len(chunk1))
resp, err = resty.R().SetHeader("Content-Type", "application/octet-stream").
SetHeader("Content-Range", contentRange).SetBody(chunk1).Patch(BaseURL + loc)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 202)
// check progress
resp, err = resty.R().Get(BaseURL + loc)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 204)
r := resp.Header().Get("Range")
So(r, ShouldNotBeEmpty)
So(r, ShouldEqual, "bytes="+contentRange)
// write same chunk should fail
contentRange = fmt.Sprintf("%d-%d", 0, len(chunk1))
resp, err = resty.R().SetHeader("Content-Type", "application/octet-stream").
SetHeader("Content-Range", contentRange).SetBody(chunk1).Patch(BaseURL + loc)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 400)
So(resp.String(), ShouldNotBeEmpty)
chunk2 := []byte("this is the second chunk")
n, err = buf.Write(chunk2)
So(n, ShouldEqual, len(chunk2))
So(err, ShouldBeNil)
digest := godigest.FromBytes(buf.Bytes())
So(digest, ShouldNotBeNil)
// write final chunk
contentRange = fmt.Sprintf("%d-%d", len(chunk1), len(buf.Bytes()))
resp, err = resty.R().SetQueryParam("digest", digest.String()).
SetHeader("Content-Range", contentRange).
SetHeader("Content-Type", "application/octet-stream").SetBody(chunk2).Put(BaseURL + loc)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 201)
blobLoc := resp.Header().Get("Location")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 201)
So(blobLoc, ShouldNotBeEmpty)
So(resp.Header().Get("Content-Length"), ShouldEqual, "0")
So(resp.Header().Get(api.DistContentDigestKey), ShouldNotBeEmpty)
// upload reference should now be removed
resp, err = resty.R().Get(BaseURL + loc)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 404)
// blob reference should be accessible
resp, err = resty.R().Get(BaseURL + blobLoc)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
})
Convey("Create and delete uploads", func() { Convey("Create and delete uploads", func() {
// create a upload // create a upload
resp, err := resty.R().Post(BaseURL + "/v2/repo/blobs/uploads/") resp, err := resty.R().Post(BaseURL + "/v2/repo/blobs/uploads/")