From 066bf1b9ebccee83ae5ee2aeb41cbc7cf8621b58 Mon Sep 17 00:00:00 2001 From: Ramkumar Chinchani Date: Tue, 9 Jul 2019 22:23:59 -0700 Subject: [PATCH] router: move to gorilla/mux to support multiple name path components --- Makefile | 2 +- README.md | 1 - WORKSPACE | 32 ++- docs/docs.go | 2 +- examples/config-test.json | 2 +- go.mod | 15 +- go.sum | 59 +--- pkg/api/BUILD.bazel | 7 +- pkg/api/auth.go | 86 +++--- pkg/api/controller.go | 13 +- pkg/api/controller_test.go | 6 +- pkg/api/log.go | 93 +++--- pkg/api/regexp.go | 73 +++++ pkg/api/routes.go | 566 +++++++++++++++++++------------------ pkg/api/routes_test.go | 121 +++++++- 15 files changed, 660 insertions(+), 418 deletions(-) create mode 100644 pkg/api/regexp.go diff --git a/Makefile b/Makefile index c5f8d7cbd..8b5c813be 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,7 @@ binary: doc .PHONY: debug 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 test: diff --git a/README.md b/README.md index 3c5a44998..1f198064b 100644 --- a/README.md +++ b/README.md @@ -12,5 +12,4 @@ # Caveats * 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 diff --git a/WORKSPACE b/WORKSPACE index 27ca2559f..df6db2502 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -575,7 +575,7 @@ go_repository( go_repository( name = "com_github_smartystreets_goconvey", - commit = "68dc04aab96a", + commit = "9d28bd7c0945", importpath = "github.com/smartystreets/goconvey", ) @@ -648,7 +648,7 @@ go_repository( go_repository( name = "com_github_swaggo_swag", importpath = "github.com/swaggo/swag", - tag = "v1.5.1", + tag = "v1.6.2", ) go_repository( @@ -660,7 +660,7 @@ go_repository( go_repository( name = "com_github_ugorji_go", importpath = "github.com/ugorji/go", - tag = "v1.1.5-pre", + tag = "v1.1.4", ) go_repository( @@ -761,7 +761,7 @@ go_repository( go_repository( name = "org_golang_x_crypto", - commit = "ea8f1a30c443", + commit = "4def268fd1a4", importpath = "golang.org/x/crypto", ) @@ -830,3 +830,27 @@ go_repository( importpath = "go.uber.org/zap", 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", +) diff --git a/docs/docs.go b/docs/docs.go index b950265a7..1c85c93f3 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -1,6 +1,6 @@ // GENERATED BY THE COMMAND ABOVE; DO NOT EDIT // 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 diff --git a/examples/config-test.json b/examples/config-test.json index dd306b46d..00519969b 100644 --- a/examples/config-test.json +++ b/examples/config-test.json @@ -4,7 +4,7 @@ "rootDirectory":"/tmp/zot" }, "http": { - "address":"127.0.0.1", + "address":"0.0.0.0", "port":"8080" }, "log":{ diff --git a/go.mod b/go.mod index 5aee2a852..67ff5a6f8 100644 --- a/go.mod +++ b/go.mod @@ -4,19 +4,22 @@ go 1.12 require ( 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/gorilla/mux v1.7.3 + github.com/json-iterator/go v1.1.6 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/go-digest v1.0.0-rc1 github.com/opencontainers/image-spec v1.0.1 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/viper v1.4.0 - github.com/swaggo/gin-swagger v1.1.0 - github.com/swaggo/swag v1.5.1 - github.com/ugorji/go v1.1.5-pre // indirect - golang.org/x/crypto v0.0.0-20190618222545-ea8f1a30c443 + github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14 // indirect + github.com/swaggo/http-swagger v0.0.0-20190614090009-c2865af9083e + github.com/swaggo/swag v1.6.2 + golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 gopkg.in/resty.v1 v1.12.0 ) diff --git a/go.sum b/go.sum index 8c9edec7c..0dc5c1085 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,7 @@ 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/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/PuerkitoBio/purell v1.1.0 h1:rmGxhojJlM0tuKtfdvliR84CFHljx9ag64t2xmVkjK4= 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/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.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/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/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-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-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/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/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/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= 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/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.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= 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/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/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/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/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/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/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 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/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/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 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/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 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/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/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/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 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.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 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/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= @@ -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/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/goconvey v0.0.0-20190330032615-68dc04aab96a h1:pa8hGb/2YqsZKovtsgrwcDH1RZhVbTKCjLp47XpqCDs= -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 h1:N8Bg45zpk/UcpNGnfJt2y/3lRWASHNTUET8owPYCgYI= +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/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= @@ -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.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 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/swaggo/gin-swagger v1.1.0 h1:ZI6/82S07DkkrMfGKbJhKj1R+QNTICkeAJP06pU36pU= -github.com/swaggo/gin-swagger v1.1.0/go.mod h1:FQlm07YuT1glfN3hQiO11UQ2m39vOCZ/aa3WWr5E+XU= -github.com/swaggo/swag v1.4.0/go.mod h1:hog2WgeMOrQ/LvQ+o1YGTeT+vWVrbi0SiIslBtxKTyM= -github.com/swaggo/swag v1.5.1 h1:2Agm8I4K5qb00620mHq0VJ05/KT4FtmALPIcQR9lEZM= -github.com/swaggo/swag v1.5.1/go.mod h1:1Bl9F/ZBpVWh22nY0zmYyASPO1lI/zIwRDrpZU+tv8Y= +github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14 h1:PyYN9JH5jY9j6av01SpfRMb+1DWg/i3MbGOKPxJ2wjM= +github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14/go.mod h1:gxQT6pBGRuIGunNf/+tSOB5OHvguWi8Tbt82WOkf35E= +github.com/swaggo/http-swagger v0.0.0-20190614090009-c2865af9083e h1:m5sYJ43teIUlESuKRFQRRm7kqi6ExiYwVKfoXNuRgHU= +github.com/swaggo/http-swagger v0.0.0-20190614090009-c2865af9083e/go.mod h1:eycbshptIv+tqTMlLEaGC2noPNcetbrcYEelLafrIDI= +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/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.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-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/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= @@ -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-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-20190618222545-ea8f1a30c443 h1:IcSOAf4PyMp3U3XbIEj1/xJ2BjNN2jWv7JoyOsMxXUU= -golang.org/x/crypto v0.0.0-20190618222545-ea8f1a30c443/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc= +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-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-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-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-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/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 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-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-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/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 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/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-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-20190311212946-11955173bddd/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= 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 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/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/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= diff --git a/pkg/api/BUILD.bazel b/pkg/api/BUILD.bazel index 6ea8f6bf5..a08b1de11 100644 --- a/pkg/api/BUILD.bazel +++ b/pkg/api/BUILD.bazel @@ -8,6 +8,7 @@ go_library( "controller.go", "errors.go", "log.go", + "regexp.go", "routes.go", ], importpath = "github.com/anuvu/zot/pkg/api", @@ -16,12 +17,12 @@ go_library( "//docs:go_default_library", "//errors: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_image_spec//specs-go/v1:go_default_library", "@com_github_rs_zerolog//:go_default_library", - "@com_github_swaggo_gin_swagger//:go_default_library", - "@com_github_swaggo_gin_swagger//swaggerFiles:go_default_library", + "@com_github_swaggo_http_swagger//:go_default_library", "@org_golang_x_crypto//bcrypt:go_default_library", ], ) diff --git a/pkg/api/auth.go b/pkg/api/auth.go index cf337aae4..28a7ff977 100644 --- a/pkg/api/auth.go +++ b/pkg/api/auth.go @@ -9,20 +9,25 @@ import ( "strings" "time" - "github.com/gin-gonic/gin" + "github.com/gorilla/mux" "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) - ginCtx.Header("WWW-Authenticate", realm) - ginCtx.AbortWithStatusJSON(http.StatusUnauthorized, NewError(UNAUTHORIZED)) + w.Header().Set("WWW-Authenticate", realm) + 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 == "" { // 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] } - return func(ginCtx *gin.Context) { - basicAuth := ginCtx.Request.Header.Get("Authorization") - if basicAuth == "" { - authFail(ginCtx, realm, delay) - return - } + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + basicAuth := r.Header.Get("Authorization") + if basicAuth == "" { + authFail(w, realm, delay) + return + } - s := strings.SplitN(basicAuth, " ", 2) - if len(s) != 2 || strings.ToLower(s[0]) != "basic" { - authFail(ginCtx, realm, delay) - return - } + s := strings.SplitN(basicAuth, " ", 2) + if len(s) != 2 || strings.ToLower(s[0]) != "basic" { + authFail(w, realm, delay) + return + } - b, err := base64.StdEncoding.DecodeString(s[1]) - if err != nil { - authFail(ginCtx, realm, delay) - return - } + b, err := base64.StdEncoding.DecodeString(s[1]) + if err != nil { + authFail(w, realm, delay) + return + } - pair := strings.SplitN(string(b), ":", 2) - if len(pair) != 2 { - authFail(ginCtx, realm, delay) - return - } + pair := strings.SplitN(string(b), ":", 2) + if len(pair) != 2 { + authFail(w, realm, delay) + return + } - username := pair[0] - passphrase := pair[1] + username := pair[0] + passphrase := pair[1] - passphraseHash, ok := credMap[username] - if !ok { - authFail(ginCtx, realm, delay) - return - } + passphraseHash, ok := credMap[username] + if !ok { + authFail(w, realm, delay) + return + } - if err := bcrypt.CompareHashAndPassword([]byte(passphraseHash), []byte(passphrase)); err != nil { - authFail(ginCtx, realm, delay) - return - } + if err := bcrypt.CompareHashAndPassword([]byte(passphraseHash), []byte(passphrase)); err != nil { + authFail(w, realm, delay) + return + } + + // Process request + next.ServeHTTP(w, r) + }) } } diff --git a/pkg/api/controller.go b/pkg/api/controller.go index 2253515f5..2e01dc178 100644 --- a/pkg/api/controller.go +++ b/pkg/api/controller.go @@ -9,13 +9,13 @@ import ( "net/http" "github.com/anuvu/zot/pkg/storage" - "github.com/gin-gonic/gin" + "github.com/gorilla/mux" "github.com/rs/zerolog" ) type Controller struct { Config *Config - Router *gin.Engine + Router *mux.Router ImageStore *storage.ImageStore Log zerolog.Logger Server *http.Server @@ -26,13 +26,8 @@ func NewController(config *Config) *Controller { } func (c *Controller) Run() error { - if c.Config.Log.Level == "debug" { - gin.SetMode(gin.DebugMode) - } else { - gin.SetMode(gin.ReleaseMode) - } - engine := gin.New() - engine.Use(gin.Recovery(), Logger(c.Log)) + engine := mux.NewRouter() + engine.Use(Logger(c.Log)) c.Router = engine _ = NewRouteHandler(c) diff --git a/pkg/api/controller_test.go b/pkg/api/controller_test.go index c2a1596b0..ebe6044b4 100644 --- a/pkg/api/controller_test.go +++ b/pkg/api/controller_test.go @@ -66,7 +66,7 @@ func TestBasicAuth(t *testing.T) { }() // without creds, should get access error - resp, err := resty.R().Get(BaseURL1) + resp, err := resty.R().Get(BaseURL1 + "/v2/") So(err, ShouldBeNil) So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 401) @@ -135,7 +135,7 @@ func TestTLSWithBasicAuth(t *testing.T) { So(resp.StatusCode(), ShouldEqual, 400) // without creds, should get access error - resp, err = resty.R().Get(BaseSecureURL2) + resp, err = resty.R().Get(BaseSecureURL2 + "/v2/") So(err, ShouldBeNil) So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 401) @@ -220,7 +220,7 @@ func TestTLSMutualAuth(t *testing.T) { defer func() { resty.SetCertificates(tls.Certificate{}) }() // 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(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 401) diff --git a/pkg/api/log.go b/pkg/api/log.go index 6061d3e30..db6a72ae0 100644 --- a/pkg/api/log.go +++ b/pkg/api/log.go @@ -1,10 +1,11 @@ package api import ( + "net/http" "os" "time" - "github.com/gin-gonic/gin" + "github.com/gorilla/mux" "github.com/rs/zerolog" ) @@ -28,43 +29,65 @@ func NewLogger(config *Config) zerolog.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() - return func(ginCtx *gin.Context) { - // Start timer - start := time.Now() - path := ginCtx.Request.URL.Path - raw := ginCtx.Request.URL.RawQuery + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Start timer + start := time.Now() + path := r.URL.Path + raw := r.URL.RawQuery - // Process request - ginCtx.Next() + sw := statusWriter{ResponseWriter: w} - // Stop timer - end := time.Now() - 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 - } + // Process request + next.ServeHTTP(&sw, r) + + // Stop timer + end := time.Now() + latency := end.Sub(start) + if latency > time.Minute { + // Truncate in a golang < 1.8 safe way + latency -= latency % time.Second + } + clientIP := r.RemoteAddr + method := r.Method + 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("errMsg", errMsg). - Str("latency", latency.String()). - Int("bodySize", bodySize). - Interface("headers", headers). - Msg("HTTP API") + 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") + }) } } diff --git a/pkg/api/regexp.go b/pkg/api/regexp.go new file mode 100644 index 000000000..e2df041e9 --- /dev/null +++ b/pkg/api/regexp.go @@ -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() + `)`) +} diff --git a/pkg/api/routes.go b/pkg/api/routes.go index c95384847..f2ecb157d 100644 --- a/pkg/api/routes.go +++ b/pkg/api/routes.go @@ -13,6 +13,8 @@ package api import ( "fmt" + "io" + "io/ioutil" "net/http" "path" "strconv" @@ -20,10 +22,10 @@ import ( _ "github.com/anuvu/zot/docs" // nolint (golint) - as required by swaggo "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" - ginSwagger "github.com/swaggo/gin-swagger" - "github.com/swaggo/gin-swagger/swaggerFiles" + httpSwagger "github.com/swaggo/http-swagger" ) const RoutePrefix = "/v2" @@ -43,36 +45,41 @@ func NewRouteHandler(c *Controller) *RouteHandler { func (rh *RouteHandler) SetupRoutes() { 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.GET("/:name/tags/list", rh.ListTags) - g.HEAD("/:name/manifests/:reference", rh.CheckManifest) - g.GET("/:name/manifests/:reference", rh.GetManifest) - g.PUT("/:name/manifests/:reference", rh.UpdateManifest) - g.DELETE("/:name/manifests/:reference", rh.DeleteManifest) - g.HEAD("/:name/blobs/:digest", rh.CheckBlob) - g.GET("/:name/blobs/:digest", rh.GetBlob) - g.DELETE("/:name/blobs/:digest", rh.DeleteBlob) - - // NOTE: some routes as per the spec need to be setup with URL params which - // must equal specific keywords - - // route for POST "/v2/:name/blobs/uploads/" and param ":digest"="uploads" - g.POST("/:name/blobs/:digest/", rh.CreateBlobUpload) - // route for GET "/v2/:name/blobs/uploads/:uuid" and param ":digest"="uploads" - g.GET("/:name/blobs/:digest/:uuid", rh.GetBlobUpload) - // route for PATCH "/v2/:name/blobs/uploads/:uuid" and param ":digest"="uploads" - g.PATCH("/:name/blobs/:digest/:uuid", rh.PatchBlobUpload) - // route for PUT "/v2/:name/blobs/uploads/:uuid" and param ":digest"="uploads" - g.PUT("/:name/blobs/:digest/:uuid", rh.UpdateBlobUpload) - // route for DELETE "/v2/:name/blobs/uploads/:uuid" and param ":digest"="uploads" - g.DELETE("/:name/blobs/:digest/:uuid", rh.DeleteBlobUpload) - // route for GET "/v2/_catalog" and param ":name"="_catalog" - g.GET("/:name", rh.ListRepositories) + g.HandleFunc(fmt.Sprintf("/{name:%s}/tags/list", NameRegexp.String()), + rh.ListTags).Methods("GET") + g.HandleFunc(fmt.Sprintf("/{name:%s}/manifests/{reference}", NameRegexp.String()), + rh.CheckManifest).Methods("HEAD") + g.HandleFunc(fmt.Sprintf("/{name:%s}/manifests/{reference}", NameRegexp.String()), + rh.GetManifest).Methods("GET") + g.HandleFunc(fmt.Sprintf("/{name:%s}/manifests/{reference}", NameRegexp.String()), + rh.UpdateManifest).Methods("PUT") + g.HandleFunc(fmt.Sprintf("/{name:%s}/manifests/{reference}", NameRegexp.String()), + rh.DeleteManifest).Methods("DELETE") + g.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/{digest}", NameRegexp.String()), + rh.CheckBlob).Methods("HEAD") + g.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/{digest}", NameRegexp.String()), + rh.GetBlob).Methods("GET") + g.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/{digest}", NameRegexp.String()), + rh.DeleteBlob).Methods("DELETE") + g.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/uploads/", NameRegexp.String()), + rh.CreateBlobUpload).Methods("POST") + g.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/uploads/{uuid}", NameRegexp.String()), + rh.GetBlobUpload).Methods("GET") + g.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/uploads/{uuid}", NameRegexp.String()), + rh.PatchBlobUpload).Methods("PATCH") + g.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/uploads/{uuid}", NameRegexp.String()), + rh.UpdateBlobUpload).Methods("PUT") + 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" - rh.c.Router.GET("/swagger/v2/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) + rh.c.Router.PathPrefix("/swagger/v2/").Methods("GET").Handler(httpSwagger.WrapHandler) } // Method handlers @@ -84,9 +91,9 @@ func (rh *RouteHandler) SetupRoutes() { // @Accept json // @Produce json // @Success 200 {string} string "ok" -func (rh *RouteHandler) CheckVersionSupport(ginCtx *gin.Context) { - ginCtx.Data(http.StatusOK, "application/json", []byte{}) - ginCtx.Header(DistAPIVersion, "registry/2.0") +func (rh *RouteHandler) CheckVersionSupport(w http.ResponseWriter, r *http.Request) { + w.Header().Set(DistAPIVersion, "registry/2.0") + WriteData(w, http.StatusOK, "application/json", []byte{}) } type ImageTags struct { @@ -103,20 +110,21 @@ type ImageTags struct { // @Param name path string true "test" // @Success 200 {object} api.ImageTags // @Failure 404 {string} string "not found" -func (rh *RouteHandler) ListTags(ginCtx *gin.Context) { - name := ginCtx.Param("name") - if name == "" { - ginCtx.Status(http.StatusNotFound) +func (rh *RouteHandler) ListTags(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + name, ok := vars["name"] + if !ok || name == "" { + w.WriteHeader(http.StatusNotFound) return } tags, err := rh.c.ImageStore.GetImageTags(name) 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 } - ginCtx.JSON(http.StatusOK, ImageTags{Name: name, Tags: tags}) + WriteJSON(w, http.StatusOK, ImageTags{Name: name, Tags: tags}) } // CheckManifest godoc @@ -131,16 +139,17 @@ func (rh *RouteHandler) ListTags(ginCtx *gin.Context) { // @Header 200 {object} api.DistContentDigestKey // @Failure 404 {string} string "not found" // @Failure 500 {string} string "internal server error" -func (rh *RouteHandler) CheckManifest(ginCtx *gin.Context) { - name := ginCtx.Param("name") - if name == "" { - ginCtx.Status(http.StatusNotFound) +func (rh *RouteHandler) CheckManifest(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + name, ok := vars["name"] + if !ok || name == "" { + w.WriteHeader(http.StatusNotFound) return } - reference := ginCtx.Param("reference") - if reference == "" { - ginCtx.JSON(http.StatusNotFound, NewError(MANIFEST_INVALID, map[string]string{"reference": reference})) + reference, ok := vars["reference"] + if !ok || reference == "" { + WriteJSON(w, http.StatusNotFound, NewError(MANIFEST_INVALID, map[string]string{"reference": reference})) return } @@ -148,16 +157,16 @@ func (rh *RouteHandler) CheckManifest(ginCtx *gin.Context) { if err != nil { switch err { 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: - 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 } - ginCtx.Status(http.StatusOK) - ginCtx.Header(DistContentDigestKey, digest) - ginCtx.Header("Content-Length", "0") + w.Header().Set(DistContentDigestKey, digest) + w.Header().Set("Content-Length", "0") + w.WriteHeader(http.StatusOK) } // NOTE: https://github.com/swaggo/swag/issues/387 @@ -177,16 +186,17 @@ type ImageManifest struct { // @Failure 404 {string} string "not found" // @Failure 500 {string} string "internal server error" // @Router /v2/{name}/manifests/{reference} [get] -func (rh *RouteHandler) GetManifest(ginCtx *gin.Context) { - name := ginCtx.Param("name") - if name == "" { - ginCtx.Status(http.StatusNotFound) +func (rh *RouteHandler) GetManifest(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + name, ok := vars["name"] + if !ok || name == "" { + w.WriteHeader(http.StatusNotFound) return } - reference := ginCtx.Param("reference") - if reference == "" { - ginCtx.JSON(http.StatusNotFound, NewError(MANIFEST_UNKNOWN, map[string]string{"reference": reference})) + reference, ok := vars["reference"] + if !ok || reference == "" { + WriteJSON(w, http.StatusNotFound, NewError(MANIFEST_UNKNOWN, map[string]string{"reference": reference})) return } @@ -194,19 +204,19 @@ func (rh *RouteHandler) GetManifest(ginCtx *gin.Context) { if err != nil { switch err { 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: - 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: - 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: - ginCtx.Status(http.StatusInternalServerError) + w.WriteHeader(http.StatusInternalServerError) } return } - ginCtx.Data(http.StatusOK, mediaType, content) - ginCtx.Header(DistContentDigestKey, digest) + WriteData(w, http.StatusOK, mediaType, content) + w.Header().Set(DistContentDigestKey, digest) } // UpdateManifest godoc @@ -222,28 +232,29 @@ func (rh *RouteHandler) GetManifest(ginCtx *gin.Context) { // @Failure 404 {string} string "not found" // @Failure 500 {string} string "internal server error" // @Router /v2/{name}/manifests/{reference} [put] -func (rh *RouteHandler) UpdateManifest(ginCtx *gin.Context) { - name := ginCtx.Param("name") - if name == "" { - ginCtx.Status(http.StatusNotFound) +func (rh *RouteHandler) UpdateManifest(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + name, ok := vars["name"] + if !ok || name == "" { + w.WriteHeader(http.StatusNotFound) return } - reference := ginCtx.Param("reference") - if reference == "" { - ginCtx.JSON(http.StatusNotFound, NewError(MANIFEST_INVALID, map[string]string{"reference": reference})) + reference, ok := vars["reference"] + if !ok || reference == "" { + WriteJSON(w, http.StatusNotFound, NewError(MANIFEST_INVALID, map[string]string{"reference": reference})) return } - mediaType := ginCtx.ContentType() + mediaType := r.Header.Get("Content-Type") if mediaType != ispec.MediaTypeImageManifest { - ginCtx.Status(http.StatusUnsupportedMediaType) + w.WriteHeader(http.StatusUnsupportedMediaType) return } - body, err := ginCtx.GetRawData() + body, err := ioutil.ReadAll(r.Body) if err != nil { - ginCtx.Status(http.StatusInternalServerError) + w.WriteHeader(http.StatusInternalServerError) return } @@ -251,22 +262,22 @@ func (rh *RouteHandler) UpdateManifest(ginCtx *gin.Context) { if err != nil { switch err { 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: - 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: - 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: - 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: - ginCtx.Status(http.StatusInternalServerError) + w.WriteHeader(http.StatusInternalServerError) } return } - ginCtx.Status(http.StatusCreated) - ginCtx.Header("Location", fmt.Sprintf("/v2/%s/manifests/%s", name, digest)) - ginCtx.Header(DistContentDigestKey, digest) + w.Header().Set("Location", fmt.Sprintf("/v2/%s/manifests/%s", name, digest)) + w.Header().Set(DistContentDigestKey, digest) + w.WriteHeader(http.StatusCreated) } // DeleteManifest godoc @@ -278,16 +289,17 @@ func (rh *RouteHandler) UpdateManifest(ginCtx *gin.Context) { // @Param reference path string true "image reference or digest" // @Success 200 {string} string "ok" // @Router /v2/{name}/manifests/{reference} [delete] -func (rh *RouteHandler) DeleteManifest(ginCtx *gin.Context) { - name := ginCtx.Param("name") - if name == "" { - ginCtx.Status(http.StatusNotFound) +func (rh *RouteHandler) DeleteManifest(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + name, ok := vars["name"] + if !ok || name == "" { + w.WriteHeader(http.StatusNotFound) return } - reference := ginCtx.Param("reference") - if reference == "" { - ginCtx.Status(http.StatusNotFound) + reference, ok := vars["reference"] + if !ok || reference == "" { + w.WriteHeader(http.StatusNotFound) return } @@ -295,16 +307,16 @@ func (rh *RouteHandler) DeleteManifest(ginCtx *gin.Context) { if err != nil { switch err { 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: - 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: - ginCtx.Status(http.StatusInternalServerError) + w.WriteHeader(http.StatusInternalServerError) } return } - ginCtx.Status(http.StatusOK) + w.WriteHeader(http.StatusOK) } // CheckBlob godoc @@ -317,44 +329,45 @@ func (rh *RouteHandler) DeleteManifest(ginCtx *gin.Context) { // @Success 200 {object} api.ImageManifest // @Header 200 {object} api.DistContentDigestKey // @Router /v2/{name}/blobs/{digest} [head] -func (rh *RouteHandler) CheckBlob(ginCtx *gin.Context) { - name := ginCtx.Param("name") - if name == "" { - ginCtx.Status(http.StatusNotFound) +func (rh *RouteHandler) CheckBlob(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + name, ok := vars["name"] + if !ok || name == "" { + w.WriteHeader(http.StatusNotFound) return } - digest := ginCtx.Param("digest") - if digest == "" { - ginCtx.Status(http.StatusNotFound) + digest, ok := vars["digest"] + if !ok || digest == "" { + w.WriteHeader(http.StatusNotFound) return } - mediaType := ginCtx.Request.Header.Get("Accept") + mediaType := r.Header.Get("Accept") ok, blen, err := rh.c.ImageStore.CheckBlob(name, digest, mediaType) if err != nil { switch err { 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: - 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: - 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: - ginCtx.Status(http.StatusInternalServerError) + w.WriteHeader(http.StatusInternalServerError) } return } 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 } - ginCtx.Status(http.StatusOK) - ginCtx.Header("Content-Length", fmt.Sprintf("%d", blen)) - ginCtx.Header(DistContentDigestKey, digest) + w.Header().Set("Content-Length", fmt.Sprintf("%d", blen)) + w.Header().Set(DistContentDigestKey, digest) + w.WriteHeader(http.StatusOK) } // GetBlob godoc @@ -367,41 +380,41 @@ func (rh *RouteHandler) CheckBlob(ginCtx *gin.Context) { // @Header 200 {object} api.DistContentDigestKey // @Success 200 {object} api.ImageManifest // @Router /v2/{name}/blobs/{digest} [get] -func (rh *RouteHandler) GetBlob(ginCtx *gin.Context) { - name := ginCtx.Param("name") - if name == "" { - ginCtx.Status(http.StatusNotFound) +func (rh *RouteHandler) GetBlob(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + name, ok := vars["name"] + if !ok || name == "" { + w.WriteHeader(http.StatusNotFound) return } - digest := ginCtx.Param("digest") - if digest == "" { - ginCtx.Status(http.StatusNotFound) + digest, ok := vars["digest"] + if !ok || digest == "" { + w.WriteHeader(http.StatusNotFound) return } - mediaType := ginCtx.Request.Header.Get("Accept") + mediaType := r.Header.Get("Accept") br, blen, err := rh.c.ImageStore.GetBlob(name, digest, mediaType) if err != nil { switch err { 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: - 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: - 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: - ginCtx.Status(http.StatusInternalServerError) + w.WriteHeader(http.StatusInternalServerError) } return } - ginCtx.Status(http.StatusOK) - ginCtx.Header("Content-Length", fmt.Sprintf("%d", blen)) - ginCtx.Header(DistContentDigestKey, digest) + w.Header().Set("Content-Length", fmt.Sprintf("%d", blen)) + w.Header().Set(DistContentDigestKey, digest) // return the blob data - ginCtx.DataFromReader(http.StatusOK, blen, mediaType, br, map[string]string{}) + WriteDataFromReader(w, http.StatusOK, blen, mediaType, br) } // DeleteBlob godoc @@ -413,16 +426,17 @@ func (rh *RouteHandler) GetBlob(ginCtx *gin.Context) { // @Param digest path string true "blob/layer digest" // @Success 202 {string} string "accepted" // @Router /v2/{name}/blobs/{digest} [delete] -func (rh *RouteHandler) DeleteBlob(ginCtx *gin.Context) { - name := ginCtx.Param("name") - if name == "" { - ginCtx.Status(http.StatusNotFound) +func (rh *RouteHandler) DeleteBlob(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + name, ok := vars["name"] + if !ok || name == "" { + w.WriteHeader(http.StatusNotFound) return } - digest := ginCtx.Param("digest") - if digest == "" { - ginCtx.Status(http.StatusNotFound) + digest, ok := vars["digest"] + if !ok || digest == "" { + w.WriteHeader(http.StatusNotFound) return } @@ -430,18 +444,18 @@ func (rh *RouteHandler) DeleteBlob(ginCtx *gin.Context) { if err != nil { switch err { 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: - 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: - 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: - ginCtx.Status(http.StatusInternalServerError) + w.WriteHeader(http.StatusInternalServerError) } return } - ginCtx.Status(http.StatusAccepted) + w.WriteHeader(http.StatusAccepted) } // CreateBlobUpload godoc @@ -456,15 +470,11 @@ func (rh *RouteHandler) DeleteBlob(ginCtx *gin.Context) { // @Failure 404 {string} string "not found" // @Failure 500 {string} string "internal server error" // @Router /v2/{name}/blobs/uploads [post] -func (rh *RouteHandler) CreateBlobUpload(ginCtx *gin.Context) { - if paramIsNot(ginCtx, "digest", "uploads") { - ginCtx.Status(http.StatusNotFound) - return - } - - name := ginCtx.Param("name") - if name == "" { - ginCtx.Status(http.StatusNotFound) +func (rh *RouteHandler) CreateBlobUpload(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + name, ok := vars["name"] + if !ok || name == "" { + w.WriteHeader(http.StatusNotFound) return } @@ -472,16 +482,16 @@ func (rh *RouteHandler) CreateBlobUpload(ginCtx *gin.Context) { if err != nil { switch err { 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: - ginCtx.Status(http.StatusInternalServerError) + w.WriteHeader(http.StatusInternalServerError) } return } - ginCtx.Status(http.StatusAccepted) - ginCtx.Header("Location", path.Join(ginCtx.Request.URL.String(), u)) - ginCtx.Header("Range", "bytes=0-0") + w.Header().Set("Location", path.Join(r.URL.String(), u)) + w.Header().Set("Range", "bytes=0-0") + w.WriteHeader(http.StatusAccepted) } // GetBlobUpload godoc @@ -497,21 +507,17 @@ func (rh *RouteHandler) CreateBlobUpload(ginCtx *gin.Context) { // @Failure 404 {string} string "not found" // @Failure 500 {string} string "internal server error" // @Router /v2/{name}/blobs/uploads/{uuid} [get] -func (rh *RouteHandler) GetBlobUpload(ginCtx *gin.Context) { - if paramIsNot(ginCtx, "digest", "uploads") { - ginCtx.Status(http.StatusNotFound) +func (rh *RouteHandler) GetBlobUpload(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + name, ok := vars["name"] + if !ok || name == "" { + w.WriteHeader(http.StatusNotFound) return } - name := ginCtx.Param("name") - if name == "" { - ginCtx.Status(http.StatusNotFound) - return - } - - uuid := ginCtx.Param("uuid") - if uuid == "" { - ginCtx.Status(http.StatusNotFound) + uuid, ok := vars["uuid"] + if !ok || uuid == "" { + w.WriteHeader(http.StatusNotFound) return } @@ -519,22 +525,22 @@ func (rh *RouteHandler) GetBlobUpload(ginCtx *gin.Context) { if err != nil { switch err { 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: - 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: - 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: - 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: - ginCtx.Status(http.StatusInternalServerError) + w.WriteHeader(http.StatusInternalServerError) } return } - ginCtx.Status(http.StatusNoContent) - ginCtx.Header("Location", path.Join(ginCtx.Request.URL.String(), uuid)) - ginCtx.Header("Range", fmt.Sprintf("bytes=0-%d", size)) + w.Header().Set("Location", path.Join(r.URL.String(), uuid)) + w.Header().Set("Range", fmt.Sprintf("bytes=0-%d", size)) + w.WriteHeader(http.StatusNoContent) } // PatchBlobUpload godoc @@ -553,72 +559,67 @@ func (rh *RouteHandler) GetBlobUpload(ginCtx *gin.Context) { // @Failure 416 {string} string "range not satisfiable" // @Failure 500 {string} string "internal server error" // @Router /v2/{name}/blobs/uploads/{uuid} [patch] -func (rh *RouteHandler) PatchBlobUpload(ginCtx *gin.Context) { - - rh.c.Log.Info().Interface("headers", ginCtx.Request.Header).Msg("request headers") - if paramIsNot(ginCtx, "digest", "uploads") { - ginCtx.Status(http.StatusNotFound) +func (rh *RouteHandler) PatchBlobUpload(w http.ResponseWriter, r *http.Request) { + rh.c.Log.Info().Interface("headers", r.Header).Msg("request headers") + vars := mux.Vars(r) + name, ok := vars["name"] + if !ok || name == "" { + w.WriteHeader(http.StatusNotFound) return } - - name := ginCtx.Param("name") - if name == "" { - ginCtx.Status(http.StatusNotFound) - return - } - uuid := ginCtx.Param("uuid") - if uuid == "" { - ginCtx.Status(http.StatusNotFound) + uuid, ok := vars["uuid"] + if !ok || uuid == "" { + w.WriteHeader(http.StatusNotFound) return } var err error var contentLength int64 - if contentLength, err = strconv.ParseInt(ginCtx.Request.Header.Get("Content-Length"), 10, 64); err != nil { - rh.c.Log.Warn().Str("actual", ginCtx.Request.Header.Get("Content-Length")).Msg("invalid content length") - ginCtx.Status(http.StatusBadRequest) + if contentLength, err = strconv.ParseInt(r.Header.Get("Content-Length"), 10, 64); err != nil { + rh.c.Log.Warn().Str("actual", r.Header.Get("Content-Length")).Msg("invalid content length") + w.WriteHeader(http.StatusBadRequest) return } - contentRange := ginCtx.Request.Header.Get("Content-Range") + contentRange := r.Header.Get("Content-Range") if contentRange == "" { - rh.c.Log.Warn().Str("actual", ginCtx.Request.Header.Get("Content-Range")).Msg("invalid content range") - ginCtx.Status(http.StatusRequestedRangeNotSatisfiable) + rh.c.Log.Warn().Str("actual", r.Header.Get("Content-Range")).Msg("invalid content range") + w.WriteHeader(http.StatusRequestedRangeNotSatisfiable) return } var from, to int64 - if from, to, err = getContentRange(ginCtx); err != nil || (to-from) != contentLength { - ginCtx.Status(http.StatusRequestedRangeNotSatisfiable) + if from, to, err = getContentRange(r); err != nil || (to-from) != contentLength { + w.WriteHeader(http.StatusRequestedRangeNotSatisfiable) return } - if ginCtx.ContentType() != "application/octet-stream" { - rh.c.Log.Warn().Str("actual", ginCtx.ContentType()).Msg("invalid media type") - ginCtx.Status(http.StatusUnsupportedMediaType) + if contentType := r.Header.Get("Content-Type"); contentType != "application/octet-stream" { + rh.c.Log.Warn().Str("actual", contentType).Str("expected", "application/octet-stream").Msg("invalid media type") + w.WriteHeader(http.StatusUnsupportedMediaType) 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 { switch err { 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: - 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: - 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: - ginCtx.Status(http.StatusInternalServerError) + w.WriteHeader(http.StatusInternalServerError) } return } - ginCtx.Status(http.StatusAccepted) - ginCtx.Header("Location", path.Join(ginCtx.Request.URL.String(), uuid)) - ginCtx.Header("Range", fmt.Sprintf("bytes=0-%d", clen)) - ginCtx.Header("Content-Length", "0") - ginCtx.Header(BlobUploadUUID, uuid) + w.Header().Set("Location", path.Join(r.URL.String(), uuid)) + w.Header().Set("Range", fmt.Sprintf("bytes=0-%d", clen)) + w.Header().Set("Content-Length", "0") + w.Header().Set(BlobUploadUUID, uuid) + w.WriteHeader(http.StatusAccepted) } // UpdateBlobUpload godoc @@ -635,105 +636,102 @@ func (rh *RouteHandler) PatchBlobUpload(ginCtx *gin.Context) { // @Failure 404 {string} string "not found" // @Failure 500 {string} string "internal server error" // @Router /v2/{name}/blobs/uploads/{uuid} [put] -func (rh *RouteHandler) UpdateBlobUpload(ginCtx *gin.Context) { - if paramIsNot(ginCtx, "digest", "uploads") { - ginCtx.Status(http.StatusNotFound) - return - } - - name := ginCtx.Param("name") - if name == "" { - ginCtx.Status(http.StatusNotFound) +func (rh *RouteHandler) UpdateBlobUpload(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + name, ok := vars["name"] + if !ok || name == "" { + w.WriteHeader(http.StatusNotFound) return } - uuid := ginCtx.Param("uuid") - if uuid == "" { - ginCtx.Status(http.StatusNotFound) + uuid, ok := vars["uuid"] + if !ok || uuid == "" { + w.WriteHeader(http.StatusNotFound) return } - digest := ginCtx.Query("digest") - if digest == "" { - ginCtx.Status(http.StatusBadRequest) + digests, ok := r.URL.Query()["digest"] + if !ok || len(digests) != 1 { + w.WriteHeader(http.StatusBadRequest) return } + digest := digests[0] 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 { contentPresent = false } contentRangePresent := true - if ginCtx.Request.Header.Get("Content-Range") == "" { + if r.Header.Get("Content-Range") == "" { contentRangePresent = false } // we expect at least one of "Content-Length" or "Content-Range" to be // present if !contentPresent && !contentRangePresent { - ginCtx.Status(http.StatusBadRequest) + w.WriteHeader(http.StatusBadRequest) return } var from, to int64 if contentPresent { - if ginCtx.ContentType() != "application/octet-stream" { - ginCtx.Status(http.StatusUnsupportedMediaType) + if r.Header.Get("Content-Type") != "application/octet-stream" { + w.WriteHeader(http.StatusUnsupportedMediaType) return } - contentRange := ginCtx.Request.Header.Get("Content-Range") + contentRange := r.Header.Get("Content-Range") if contentRange == "" { // monolithic upload from = 0 if contentLen == 0 { - ginCtx.Status(http.StatusBadRequest) + w.WriteHeader(http.StatusBadRequest) return } to = contentLen - } else if from, to, err = getContentRange(ginCtx); err != nil { // finish chunked upload - ginCtx.Status(http.StatusRequestedRangeNotSatisfiable) + } else if from, to, err = getContentRange(r); err != nil { // finish chunked upload + w.WriteHeader(http.StatusRequestedRangeNotSatisfiable) 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 { switch err { 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: - 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: - 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: - ginCtx.Status(http.StatusInternalServerError) + w.WriteHeader(http.StatusInternalServerError) } return } } // 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 { 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: - 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: - 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: - 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: - ginCtx.Status(http.StatusInternalServerError) + w.WriteHeader(http.StatusInternalServerError) } return } - ginCtx.Status(http.StatusCreated) - ginCtx.Header("Location", fmt.Sprintf("/v2/%s/blobs/%s", name, digest)) - ginCtx.Header("Content-Length", "0") - ginCtx.Header(DistContentDigestKey, digest) + w.Header().Set("Location", fmt.Sprintf("/v2/%s/blobs/%s", name, digest)) + w.Header().Set("Content-Length", "0") + w.Header().Set(DistContentDigestKey, digest) + w.WriteHeader(http.StatusCreated) } // DeleteBlobUpload godoc @@ -747,28 +745,33 @@ func (rh *RouteHandler) UpdateBlobUpload(ginCtx *gin.Context) { // @Failure 404 {string} string "not found" // @Failure 500 {string} string "internal server error" // @Router /v2/{name}/blobs/uploads/{uuid} [delete] -func (rh *RouteHandler) DeleteBlobUpload(ginCtx *gin.Context) { - if paramIsNot(ginCtx, "digest", "uploads") { - ginCtx.Status(http.StatusNotFound) +func (rh *RouteHandler) DeleteBlobUpload(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + name, ok := vars["name"] + if !ok || name == "" { + w.WriteHeader(http.StatusNotFound) return } - name := ginCtx.Param("name") - uuid := ginCtx.Param("uuid") + uuid, ok := vars["uuid"] + if !ok || uuid == "" { + w.WriteHeader(http.StatusNotFound) + return + } if err := rh.c.ImageStore.DeleteBlobUpload(name, uuid); err != nil { switch err { 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: - 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: - ginCtx.Status(http.StatusInternalServerError) + w.WriteHeader(http.StatusInternalServerError) } return } - ginCtx.Status(http.StatusOK) + w.WriteHeader(http.StatusOK) } type RepositoryList struct { @@ -783,32 +786,22 @@ type RepositoryList struct { // @Success 200 {object} api.RepositoryList // @Failure 500 {string} string "internal server error" // @Router /v2/_catalog [get] -func (rh *RouteHandler) ListRepositories(ginCtx *gin.Context) { - if paramIsNot(ginCtx, "name", "_catalog") { - ginCtx.Status(http.StatusNotFound) - return - } - +func (rh *RouteHandler) ListRepositories(w http.ResponseWriter, r *http.Request) { repos, err := rh.c.ImageStore.GetRepositories() if err != nil { - ginCtx.Status(http.StatusInternalServerError) + w.WriteHeader(http.StatusInternalServerError) return } is := RepositoryList{Repositories: repos} - ginCtx.JSON(http.StatusOK, is) + WriteJSON(w, http.StatusOK, is) } // helper routines -func paramIsNot(ginCtx *gin.Context, name string, expected string) bool { - actual := ginCtx.Param(name) - return actual != expected -} - -func getContentRange(ginCtx *gin.Context) (int64 /* from */, int64 /* to */, error) { - contentRange := ginCtx.Request.Header.Get("Content-Range") +func getContentRange(r *http.Request) (int64 /* from */, int64 /* to */, error) { + contentRange := r.Header.Get("Content-Range") tokens := strings.Split(contentRange, "-") from, err := strconv.ParseInt(tokens[0], 10, 64) if err != nil { @@ -823,3 +816,36 @@ func getContentRange(ginCtx *gin.Context) (int64 /* from */, int64 /* to */, err } 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) +} diff --git a/pkg/api/routes_test.go b/pkg/api/routes_test.go index acf18dbf6..9cad2bdd4 100644 --- a/pkg/api/routes_test.go +++ b/pkg/api/routes_test.go @@ -1,3 +1,4 @@ +// nolint (dupl) package api_test import ( @@ -49,7 +50,7 @@ func TestAPI(t *testing.T) { So(resp.StatusCode(), ShouldEqual, 404) 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/") So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 202) @@ -111,6 +112,57 @@ func TestAPI(t *testing.T) { 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() { resp, err := resty.R().Post(BaseURL + "/v2/repo/blobs/uploads/") So(err, ShouldBeNil) @@ -178,6 +230,73 @@ func TestAPI(t *testing.T) { 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() { // create a upload resp, err := resty.R().Post(BaseURL + "/v2/repo/blobs/uploads/")