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)
|
registryApi := lib.NewRegistryApi(cfg)
|
||||||
|
|
||||||
listResult, err := registryApi.ListRepositories()
|
listResult := registryApi.ListRepositories()
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
for repository := range listResult.Repositories() {
|
for repository := range listResult.Repositories() {
|
||||||
fmt.Println(repository.Name())
|
fmt.Println(repository.Name())
|
||||||
}
|
}
|
||||||
|
|
||||||
err = listResult.LastError()
|
err = listResult.LastError()
|
||||||
}
|
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
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
|
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 {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
close = response.Close
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
|
||||||
requestUrl, err := r.paginatedRequestEndpointUrl("v2/_catalog", lastApiResponse)
|
|
||||||
|
|
||||||
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) {
|
|
||||||
listResponse := &repositoryListResponse{
|
|
||||||
repositories: make(chan Repository, r.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)
|
func (r *repositoryListRequestContext) processPartialResponse(response paginatedRequestResponse, apiResponse interface{}) {
|
||||||
}()
|
for _, repositoryName := range apiResponse.(*repositoryListJsonResponse).Repositories {
|
||||||
|
response.(*repositoryListResponse).repositories <- newRepository(repositoryName)
|
||||||
return
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *repositoryListRequestContext) createResponse(api *registryApi) paginatedRequestResponse {
|
||||||
|
return &repositoryListResponse{
|
||||||
|
repositories: make(chan Repository, api.pageSize()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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