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:
parent
131e19c26d
commit
066bf1b9eb
15 changed files with 660 additions and 418 deletions
2
Makefile
2
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:
|
||||
|
|
|
@ -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
|
||||
|
|
32
WORKSPACE
32
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",
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
"rootDirectory":"/tmp/zot"
|
||||
},
|
||||
"http": {
|
||||
"address":"127.0.0.1",
|
||||
"address":"0.0.0.0",
|
||||
"port":"8080"
|
||||
},
|
||||
"log":{
|
||||
|
|
15
go.mod
15
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
|
||||
)
|
||||
|
|
59
go.sum
59
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=
|
||||
|
|
|
@ -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",
|
||||
],
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
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")
|
||||
// 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("latency", latency.String()).
|
||||
Int("bodySize", bodySize).
|
||||
Interface("headers", headers).
|
||||
Msg("HTTP API")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
73
pkg/api/regexp.go
Normal file
73
pkg/api/regexp.go
Normal 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() + `)`)
|
||||
}
|
|
@ -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)
|
||||
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
|
||||
}
|
||||
|
||||
name := ginCtx.Param("name")
|
||||
if name == "" {
|
||||
ginCtx.Status(http.StatusNotFound)
|
||||
uuid, ok := vars["uuid"]
|
||||
if !ok || uuid == "" {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
uuid := ginCtx.Param("uuid")
|
||||
if uuid == "" {
|
||||
ginCtx.Status(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)
|
||||
}
|
||||
|
|
|
@ -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/")
|
||||
|
|
Loading…
Reference in a new issue