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:
@@ -23,17 +23,13 @@ func (r *repositoriesCmd) execute(argv []string) (err error) {
|
||||
|
||||
registryApi := lib.NewRegistryApi(cfg)
|
||||
|
||||
listResult, err := registryApi.ListRepositories()
|
||||
listResult := registryApi.ListRepositories()
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
} else {
|
||||
for repository := range listResult.Repositories() {
|
||||
fmt.Println(repository.Name())
|
||||
}
|
||||
|
||||
err = listResult.LastError()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2,6 +2,10 @@ package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"git.mayflower.de/vaillant-team/docker-ls/lib"
|
||||
)
|
||||
|
||||
type tagsCmd struct {
|
||||
@@ -9,7 +13,31 @@ type tagsCmd struct {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
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() {
|
||||
return func() {
|
||||
if argstring != "" {
|
||||
argstring = " " + strings.TrimLeft(argstring, " ")
|
||||
}
|
||||
|
||||
fmt.Printf(COMMAND_USAGE_TEMPLATE, command, argstring)
|
||||
flags.PrintDefaults()
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ type AutorizationError string
|
||||
type NotImplementedByRemoteError string
|
||||
type MalformedResponseError string
|
||||
type InvalidStatusCodeError string
|
||||
type NotFoundError string
|
||||
|
||||
var genericAuthorizationError AutorizationError = "autorization failed"
|
||||
var genericMalformedResponseError MalformedResponseError = "malformed response"
|
||||
@@ -28,6 +29,14 @@ func (e InvalidStatusCodeError) Error() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
func (e NotFoundError) Error() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
func newInvalidStatusCodeError(code int) error {
|
||||
return InvalidStatusCodeError(fmt.Sprintf("invalid API response status %d", code))
|
||||
}
|
||||
|
||||
func newNotFoundError(description string) error {
|
||||
return NotFoundError(description)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,17 @@ type RepositoryListResponse interface {
|
||||
LastError() error
|
||||
}
|
||||
|
||||
type RegistryApi interface {
|
||||
ListRepositories() (RepositoryListResponse, error)
|
||||
type Tag interface {
|
||||
Name() string
|
||||
RepositoryName() string
|
||||
}
|
||||
|
||||
type TagListResponse interface {
|
||||
Tags() <-chan Tag
|
||||
LastError() error
|
||||
}
|
||||
|
||||
type RegistryApi interface {
|
||||
ListRepositories() RepositoryListResponse
|
||||
ListTags(repositoryName string) TagListResponse
|
||||
}
|
||||
|
||||
108
lib/api_paginated_request.go
Normal file
108
lib/api_paginated_request.go
Normal 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
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
type repositoryListResponse struct {
|
||||
@@ -11,10 +9,6 @@ type repositoryListResponse struct {
|
||||
err error
|
||||
}
|
||||
|
||||
type repositoryListJsonResponse struct {
|
||||
Repositories *[]string `json:"repositories"`
|
||||
}
|
||||
|
||||
func (r *repositoryListResponse) Repositories() <-chan Repository {
|
||||
return (r.repositories)
|
||||
}
|
||||
@@ -23,100 +17,68 @@ func (r *repositoryListResponse) LastError() error {
|
||||
return r.err
|
||||
}
|
||||
|
||||
func (r *registryApi) executeListRequest(url *url.URL, initialRequest bool) (response *http.Response, close bool, err error) {
|
||||
response, err = r.connector.Get(url)
|
||||
func (r *repositoryListResponse) setLastError(err error) {
|
||||
r.err = err
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
func (r *repositoryListResponse) close() {
|
||||
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 {
|
||||
case http.StatusUnauthorized, http.StatusForbidden:
|
||||
err = genericAuthorizationError
|
||||
return
|
||||
return genericAuthorizationError
|
||||
|
||||
case http.StatusNotFound:
|
||||
if initialRequest {
|
||||
err = NotImplementedByRemoteError("registry does not implement repository listings")
|
||||
return NotImplementedByRemoteError("registry does not implement repository listings")
|
||||
} else {
|
||||
err = newInvalidStatusCodeError(response.StatusCode)
|
||||
return newInvalidStatusCodeError(response.StatusCode)
|
||||
}
|
||||
|
||||
return
|
||||
|
||||
case http.StatusOK:
|
||||
return nil
|
||||
|
||||
default:
|
||||
err = newInvalidStatusCodeError(response.StatusCode)
|
||||
return
|
||||
return newInvalidStatusCodeError(response.StatusCode)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (r *registryApi) iterateRepositoryList(lastApiResponse *http.Response, listResponse *repositoryListResponse) (apiResponse *http.Response, more bool, err error) {
|
||||
requestUrl, err := r.paginatedRequestEndpointUrl("v2/_catalog", lastApiResponse)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
func (r *repositoryListRequestContext) processPartialResponse(response paginatedRequestResponse, apiResponse interface{}) {
|
||||
for _, repositoryName := range apiResponse.(*repositoryListJsonResponse).Repositories {
|
||||
response.(*repositoryListResponse).repositories <- newRepository(repositoryName)
|
||||
}
|
||||
|
||||
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) {
|
||||
listResponse := &repositoryListResponse{
|
||||
repositories: make(chan Repository, r.pageSize()),
|
||||
func (r *repositoryListRequestContext) createResponse(api *registryApi) paginatedRequestResponse {
|
||||
return &repositoryListResponse{
|
||||
repositories: make(chan Repository, api.pageSize()),
|
||||
}
|
||||
response = listResponse
|
||||
|
||||
var apiResponse *http.Response
|
||||
apiResponse, more, err := r.iterateRepositoryList(apiResponse, listResponse)
|
||||
|
||||
go func() {
|
||||
for more {
|
||||
var err error
|
||||
apiResponse, more, err = r.iterateRepositoryList(apiResponse, listResponse)
|
||||
|
||||
if err != nil {
|
||||
listResponse.err = err
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
close(listResponse.repositories)
|
||||
}()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (r *repositoryListRequestContext) createJsonResponse() validatable {
|
||||
return new(repositoryListJsonResponse)
|
||||
}
|
||||
|
||||
func (r *registryApi) ListRepositories() RepositoryListResponse {
|
||||
return r.paginatedRequest(new(repositoryListRequestContext)).(*repositoryListResponse)
|
||||
}
|
||||
|
||||
92
lib/api_tag_list.go
Normal file
92
lib/api_tag_list.go
Normal 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
21
lib/tag.go
Normal 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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user