1
0
mirror of https://github.com/mayflower/docker-ls.git synced 2025-11-28 00:01:09 +03:00

Tag listing, fix potential deadlock if the response does not respect the page size.

This commit is contained in:
Christian Speckner
2016-02-16 13:03:26 +01:00
parent 63ae11c6e0
commit f613ce4501
9 changed files with 324 additions and 92 deletions

View File

@@ -23,18 +23,14 @@ func (r *repositoriesCmd) execute(argv []string) (err error) {
registryApi := lib.NewRegistryApi(cfg) registryApi := lib.NewRegistryApi(cfg)
listResult, err := registryApi.ListRepositories() listResult := registryApi.ListRepositories()
if err != nil { for repository := range listResult.Repositories() {
return fmt.Println(repository.Name())
} else {
for repository := range listResult.Repositories() {
fmt.Println(repository.Name())
}
err = listResult.LastError()
} }
err = listResult.LastError()
return return
} }

View File

@@ -2,6 +2,10 @@ package main
import ( import (
"flag" "flag"
"fmt"
"os"
"git.mayflower.de/vaillant-team/docker-ls/lib"
) )
type tagsCmd struct { type tagsCmd struct {
@@ -9,7 +13,31 @@ type tagsCmd struct {
} }
func (r *tagsCmd) execute(argv []string) (err error) { func (r *tagsCmd) execute(argv []string) (err error) {
err = r.flags.Parse(argv) cfg := lib.NewConfig()
cfg.BindToFlags(r.flags)
if len(argv) == 0 {
r.flags.Usage()
os.Exit(1)
}
repositoryName := argv[0]
err = r.flags.Parse(argv[1:])
if err != nil {
return
}
registryApi := lib.NewRegistryApi(cfg)
listResult := registryApi.ListTags(repositoryName)
for tag := range listResult.Tags() {
fmt.Println(tag.Name())
}
err = listResult.LastError()
return return
} }

View File

@@ -3,6 +3,7 @@ package main
import ( import (
"flag" "flag"
"fmt" "fmt"
"strings"
) )
const COMMAND_USAGE_TEMPLATE = `usage: docker-ls %s%s [options] const COMMAND_USAGE_TEMPLATE = `usage: docker-ls %s%s [options]
@@ -13,6 +14,10 @@ valid options:
func commandUsage(command string, argstring string, flags *flag.FlagSet) func() { func commandUsage(command string, argstring string, flags *flag.FlagSet) func() {
return func() { return func() {
if argstring != "" {
argstring = " " + strings.TrimLeft(argstring, " ")
}
fmt.Printf(COMMAND_USAGE_TEMPLATE, command, argstring) fmt.Printf(COMMAND_USAGE_TEMPLATE, command, argstring)
flags.PrintDefaults() flags.PrintDefaults()
} }

View File

@@ -8,6 +8,7 @@ type AutorizationError string
type NotImplementedByRemoteError string type NotImplementedByRemoteError string
type MalformedResponseError string type MalformedResponseError string
type InvalidStatusCodeError string type InvalidStatusCodeError string
type NotFoundError string
var genericAuthorizationError AutorizationError = "autorization failed" var genericAuthorizationError AutorizationError = "autorization failed"
var genericMalformedResponseError MalformedResponseError = "malformed response" var genericMalformedResponseError MalformedResponseError = "malformed response"
@@ -28,6 +29,14 @@ func (e InvalidStatusCodeError) Error() string {
return string(e) return string(e)
} }
func (e NotFoundError) Error() string {
return string(e)
}
func newInvalidStatusCodeError(code int) error { func newInvalidStatusCodeError(code int) error {
return InvalidStatusCodeError(fmt.Sprintf("invalid API response status %d", code)) return InvalidStatusCodeError(fmt.Sprintf("invalid API response status %d", code))
} }
func newNotFoundError(description string) error {
return NotFoundError(description)
}

View File

@@ -9,6 +9,17 @@ type RepositoryListResponse interface {
LastError() error LastError() error
} }
type RegistryApi interface { type Tag interface {
ListRepositories() (RepositoryListResponse, error) Name() string
RepositoryName() string
}
type TagListResponse interface {
Tags() <-chan Tag
LastError() error
}
type RegistryApi interface {
ListRepositories() RepositoryListResponse
ListTags(repositoryName string) TagListResponse
} }

View File

@@ -0,0 +1,108 @@
package lib
import (
"encoding/json"
"net/http"
"net/url"
)
type validatable interface {
validate() error
}
type paginatedRequestResponse interface {
setLastError(error)
close()
}
type paginatedRequestContext interface {
path() string
validateApiResponse(response *http.Response, initialRequest bool) error
processPartialResponse(response paginatedRequestResponse, apiResponse interface{})
createResponse(api *registryApi) paginatedRequestResponse
createJsonResponse() validatable
}
func (r *registryApi) executePaginatedRequest(ctx paginatedRequestContext, url *url.URL, initialRequest bool) (response *http.Response, close bool, err error) {
response, err = r.connector.Get(url)
if err != nil {
return
}
if err == nil {
close = response.Close
err = ctx.validateApiResponse(response, initialRequest)
}
return
}
func (r *registryApi) iteratePaginatedRequest(
ctx paginatedRequestContext,
lastApiResponse *http.Response,
response paginatedRequestResponse,
) (
apiResponse *http.Response,
more bool,
err error,
) {
requestUrl, err := r.paginatedRequestEndpointUrl(ctx.path(), lastApiResponse)
if err != nil {
return
}
apiResponse, needsClose, err := r.executePaginatedRequest(ctx, requestUrl, lastApiResponse == nil)
if needsClose {
defer apiResponse.Body.Close()
}
if err != nil {
return
}
more = apiResponse.Header.Get("link") != ""
jsonResponse := ctx.createJsonResponse()
decoder := json.NewDecoder(apiResponse.Body)
err = decoder.Decode(&jsonResponse)
if err != nil {
return
}
err = jsonResponse.validate()
if err != nil {
return
}
ctx.processPartialResponse(response, jsonResponse)
return
}
func (r *registryApi) paginatedRequest(ctx paginatedRequestContext) (response paginatedRequestResponse) {
response = ctx.createResponse(r)
go func() {
var apiResponse *http.Response
var err error
more := true
for more {
apiResponse, more, err = r.iteratePaginatedRequest(ctx, apiResponse, response)
if err != nil {
response.setLastError(err)
break
}
}
response.close()
}()
return
}

View File

@@ -1,9 +1,7 @@
package lib package lib
import ( import (
"encoding/json"
"net/http" "net/http"
"net/url"
) )
type repositoryListResponse struct { type repositoryListResponse struct {
@@ -11,10 +9,6 @@ type repositoryListResponse struct {
err error err error
} }
type repositoryListJsonResponse struct {
Repositories *[]string `json:"repositories"`
}
func (r *repositoryListResponse) Repositories() <-chan Repository { func (r *repositoryListResponse) Repositories() <-chan Repository {
return (r.repositories) return (r.repositories)
} }
@@ -23,100 +17,68 @@ func (r *repositoryListResponse) LastError() error {
return r.err return r.err
} }
func (r *registryApi) executeListRequest(url *url.URL, initialRequest bool) (response *http.Response, close bool, err error) { func (r *repositoryListResponse) setLastError(err error) {
response, err = r.connector.Get(url) r.err = err
}
if err != nil { func (r *repositoryListResponse) close() {
return close(r.repositories)
}
type repositoryListJsonResponse struct {
Repositories []string `json:"repositories"`
}
func (r *repositoryListJsonResponse) validate() error {
if r.Repositories == nil {
return genericMalformedResponseError
} }
close = response.Close return nil
}
type repositoryListRequestContext struct{}
func (r *repositoryListRequestContext) path() string {
return "v2/_catalog"
}
func (r *repositoryListRequestContext) validateApiResponse(response *http.Response, initialRequest bool) error {
switch response.StatusCode { switch response.StatusCode {
case http.StatusUnauthorized, http.StatusForbidden: case http.StatusUnauthorized, http.StatusForbidden:
err = genericAuthorizationError return genericAuthorizationError
return
case http.StatusNotFound: case http.StatusNotFound:
if initialRequest { if initialRequest {
err = NotImplementedByRemoteError("registry does not implement repository listings") return NotImplementedByRemoteError("registry does not implement repository listings")
} else { } else {
err = newInvalidStatusCodeError(response.StatusCode) return newInvalidStatusCodeError(response.StatusCode)
} }
return
case http.StatusOK: case http.StatusOK:
return nil
default: default:
err = newInvalidStatusCodeError(response.StatusCode) return newInvalidStatusCodeError(response.StatusCode)
return
} }
return
} }
func (r *registryApi) iterateRepositoryList(lastApiResponse *http.Response, listResponse *repositoryListResponse) (apiResponse *http.Response, more bool, err error) { func (r *repositoryListRequestContext) processPartialResponse(response paginatedRequestResponse, apiResponse interface{}) {
requestUrl, err := r.paginatedRequestEndpointUrl("v2/_catalog", lastApiResponse) for _, repositoryName := range apiResponse.(*repositoryListJsonResponse).Repositories {
response.(*repositoryListResponse).repositories <- newRepository(repositoryName)
if err != nil {
return
} }
apiResponse, needsClose, err := r.executeListRequest(requestUrl, lastApiResponse == nil)
if needsClose {
defer apiResponse.Body.Close()
}
if err != nil {
return
}
more = apiResponse.Header.Get("link") != ""
var jsonResponse repositoryListJsonResponse
decoder := json.NewDecoder(apiResponse.Body)
err = decoder.Decode(&jsonResponse)
if err != nil {
return
}
if jsonResponse.Repositories == nil {
err = genericMalformedResponseError
return
}
for _, repositoryName := range *jsonResponse.Repositories {
listResponse.repositories <- newRepository(repositoryName)
}
return
} }
func (r *registryApi) ListRepositories() (response RepositoryListResponse, err error) { func (r *repositoryListRequestContext) createResponse(api *registryApi) paginatedRequestResponse {
listResponse := &repositoryListResponse{ return &repositoryListResponse{
repositories: make(chan Repository, r.pageSize()), repositories: make(chan Repository, api.pageSize()),
} }
response = listResponse }
var apiResponse *http.Response func (r *repositoryListRequestContext) createJsonResponse() validatable {
apiResponse, more, err := r.iterateRepositoryList(apiResponse, listResponse) return new(repositoryListJsonResponse)
}
go func() {
for more { func (r *registryApi) ListRepositories() RepositoryListResponse {
var err error return r.paginatedRequest(new(repositoryListRequestContext)).(*repositoryListResponse)
apiResponse, more, err = r.iterateRepositoryList(apiResponse, listResponse)
if err != nil {
listResponse.err = err
break
}
}
close(listResponse.repositories)
}()
return
} }

92
lib/api_tag_list.go Normal file
View File

@@ -0,0 +1,92 @@
package lib
import (
"fmt"
"net/http"
)
type tagListResponse struct {
tags chan Tag
err error
}
func (t *tagListResponse) Tags() <-chan Tag {
return t.tags
}
func (t *tagListResponse) LastError() error {
return t.err
}
func (r *tagListResponse) setLastError(err error) {
r.err = err
}
func (r *tagListResponse) close() {
close(r.tags)
}
type tagListJsonResponse struct {
RespositoryName string `json:"name"`
Tags []string `json:"tags"`
}
func (r *tagListJsonResponse) validate() error {
if r.RespositoryName == "" || r.Tags == nil {
return genericMalformedResponseError
}
return nil
}
type tagListRequestContext struct {
repositoryName string
}
func (r *tagListRequestContext) path() string {
return fmt.Sprintf("v2/%s/tags/list", r.repositoryName)
}
func (r *tagListRequestContext) validateApiResponse(response *http.Response, initialRequest bool) error {
switch response.StatusCode {
case http.StatusUnauthorized, http.StatusForbidden:
return genericAuthorizationError
case http.StatusNotFound:
if initialRequest {
return newNotFoundError(fmt.Sprintf("%s: no such repository", r.repositoryName))
} else {
return newInvalidStatusCodeError(response.StatusCode)
}
case http.StatusOK:
return nil
default:
return newInvalidStatusCodeError(response.StatusCode)
}
}
func (r *tagListRequestContext) processPartialResponse(response paginatedRequestResponse, apiResponse interface{}) {
for _, tagName := range apiResponse.(*tagListJsonResponse).Tags {
response.(*tagListResponse).tags <- newTag(tagName, r.repositoryName)
}
}
func (r *tagListRequestContext) createResponse(api *registryApi) paginatedRequestResponse {
return &tagListResponse{
tags: make(chan Tag, api.pageSize()),
}
}
func (r *tagListRequestContext) createJsonResponse() validatable {
return new(tagListJsonResponse)
}
func (r *registryApi) ListTags(repositoryName string) TagListResponse {
ctx := tagListRequestContext{
repositoryName: repositoryName,
}
return r.paginatedRequest(&ctx).(*tagListResponse)
}

21
lib/tag.go Normal file
View File

@@ -0,0 +1,21 @@
package lib
type tag struct {
name string
repositoryName string
}
func (t *tag) Name() string {
return t.name
}
func (t *tag) RepositoryName() string {
return t.repositoryName
}
func newTag(name, repositoryName string) *tag {
return &tag{
name: name,
repositoryName: repositoryName,
}
}