mirror of
https://github.com/project-zot/zot.git
synced 2024-12-16 21:56:37 -05:00
zot: initial commit
This commit is contained in:
parent
967ff15637
commit
9d4e8b4594
55 changed files with 6478 additions and 2 deletions
3
.bazel/buildozer_commands.txt
Normal file
3
.bazel/buildozer_commands.txt
Normal file
|
@ -0,0 +1,3 @@
|
|||
set race "on"|//...:%go_test
|
||||
fix unusedLoads|//...:__pkg__
|
||||
set timeout short|//...:%go_test
|
1
.bazel/code-generator/boilerplate.go.txt
Normal file
1
.bazel/code-generator/boilerplate.go.txt
Normal file
|
@ -0,0 +1 @@
|
|||
// Generated file, do not modify manually!
|
10
.bazel/golangcilint.yaml
Normal file
10
.bazel/golangcilint.yaml
Normal file
|
@ -0,0 +1,10 @@
|
|||
run:
|
||||
deadline: 60m
|
||||
skip-dirs:
|
||||
- "internal"
|
||||
|
||||
linters:
|
||||
enable-all: true
|
||||
|
||||
output:
|
||||
format: colored-line-number
|
7
.bazel/nogo-config.json
Normal file
7
.bazel/nogo-config.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"printf": {
|
||||
"exclude_files": {
|
||||
"/vendor/": "no need to vet third party code"
|
||||
}
|
||||
}
|
||||
}
|
20
.bazel/print-workspace-status.sh
Executable file
20
.bazel/print-workspace-status.sh
Executable file
|
@ -0,0 +1,20 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# This command is used by bazel as the workspace_status_command
|
||||
# to implement build stamping with git information.
|
||||
|
||||
set -o errexit
|
||||
set -o nounset
|
||||
set -o pipefail
|
||||
|
||||
GIT_COMMIT=$(git rev-parse --short HEAD)
|
||||
GIT_TAG=$(git describe --abbrev=0 --tags 2>/dev/null || echo "0.0.0")
|
||||
|
||||
# Prefix with STABLE_ so that these values are saved to stable-status.txt
|
||||
# instead of volatile-status.txt.
|
||||
# Stamped rules will be retriggered by changes to stable-status.txt, but not by
|
||||
# changes to volatile-status.txt.
|
||||
cat <<EOF
|
||||
STABLE_BUILD_GIT_COMMIT ${GIT_COMMIT-}
|
||||
STABLE_BUILD_GIT_TAG ${GIT_TAG-}
|
||||
EOF
|
4
.bazelignore
Normal file
4
.bazelignore
Normal file
|
@ -0,0 +1,4 @@
|
|||
internal
|
||||
build
|
||||
oci_staging
|
||||
ocibuilds
|
7
.bazelrc
Normal file
7
.bazelrc
Normal file
|
@ -0,0 +1,7 @@
|
|||
build --workspace_status_command .bazel/print-workspace-status.sh
|
||||
build --action_env=GO111MODULE=on
|
||||
|
||||
test --test_output=errors
|
||||
test --test_verbose_timeout_warnings
|
||||
|
||||
coverage --test_output=summary --keep_going --collect_code_coverage --combined_report=none
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -10,3 +10,6 @@
|
|||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
bin/
|
||||
bazel-*
|
||||
|
|
14
.travis.yml
Normal file
14
.travis.yml
Normal file
|
@ -0,0 +1,14 @@
|
|||
language: go
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- os: linux
|
||||
|
||||
notifications:
|
||||
email: false
|
||||
|
||||
install:
|
||||
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then wget -N https://github.com/bazelbuild/bazel/releases/download/0.26.1/bazel-0.26.1-installer-linux-x86_64.sh && chmod +x bazel-0.26.1-installer-linux-x86_64.sh && ./bazel-0.26.1-installer-linux-x86_64.sh --user; fi
|
||||
|
||||
script:
|
||||
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; make && make -f Makefile.bazel build; fi
|
100
BUILD.bazel
Normal file
100
BUILD.bazel
Normal file
|
@ -0,0 +1,100 @@
|
|||
# Disable build files generation for these directories
|
||||
# gazelle:exclude vendor/github.com/bazelbuild/buildtools/buildifier2
|
||||
# gazelle:exclude vendor/golang.org/x/tools/cmd/fiximports/testdata
|
||||
# gazelle:exclude vendor/golang.org/x/tools/go/gcimporter15/testdata
|
||||
# gazelle:exclude vendor/golang.org/x/tools/go/internal/gccgoimporter/testdata
|
||||
# gazelle:exclude vendor/golang.org/x/tools/go/loader/testdata
|
||||
# gazelle:exclude vendor/golang.org/x/tools/go/internal/gcimporter/testdata
|
||||
# gazelle:resolve proto go github.com/grpc-ecosystem/grpc-gateway/internal //internal:go_default_library
|
||||
# gazelle:proto disable_global
|
||||
|
||||
load("@bazel_gazelle//:def.bzl", "gazelle")
|
||||
load("@com_github_atlassian_bazel_tools//buildozer:def.bzl", "buildozer")
|
||||
load("@com_github_atlassian_bazel_tools//goimports:def.bzl", "goimports")
|
||||
load("@com_github_atlassian_bazel_tools//golangcilint:def.bzl", "golangcilint")
|
||||
load("@com_github_bazelbuild_buildtools//buildifier:def.bzl", "buildifier")
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_library", "nogo")
|
||||
|
||||
gazelle(
|
||||
name = "gazelle",
|
||||
build_tags = ["jsoniter"],
|
||||
external = "external",
|
||||
extra_args = ["-exclude=vendor"],
|
||||
gazelle = "@bazel_gazelle//cmd/gazelle:gazelle_pure",
|
||||
prefix = "github.com/anuvu/zot",
|
||||
)
|
||||
|
||||
gazelle(
|
||||
name = "gazelle_fix",
|
||||
build_tags = ["jsoniter"],
|
||||
command = "fix",
|
||||
external = "external",
|
||||
extra_args = ["-exclude=vendor"],
|
||||
gazelle = "@bazel_gazelle//cmd/gazelle:gazelle_pure",
|
||||
prefix = "github.com/anuvu/zot",
|
||||
)
|
||||
|
||||
buildifier(
|
||||
name = "buildifier",
|
||||
exclude_patterns = ["./vendor/*"],
|
||||
)
|
||||
|
||||
buildifier(
|
||||
name = "buildifier_check",
|
||||
exclude_patterns = ["./vendor/*"],
|
||||
mode = "check",
|
||||
)
|
||||
|
||||
buildifier(
|
||||
name = "buildifier_fix",
|
||||
lint_mode = "fix",
|
||||
)
|
||||
|
||||
buildozer(
|
||||
name = "buildozer",
|
||||
commands = ".bazel/buildozer_commands.txt",
|
||||
)
|
||||
|
||||
goimports(
|
||||
name = "goimports",
|
||||
display_diffs = True,
|
||||
exclude_files = [
|
||||
"zz_generated.*",
|
||||
],
|
||||
exclude_paths = [
|
||||
"./vendor/*",
|
||||
],
|
||||
prefix = "github.com/anuvu/zot",
|
||||
write = True,
|
||||
)
|
||||
|
||||
golangcilint(
|
||||
name = "golangcilint",
|
||||
config = ".bazel/golangcilint.yaml",
|
||||
paths = [
|
||||
"./...",
|
||||
],
|
||||
prefix = "github.com/anuvu/zot",
|
||||
)
|
||||
|
||||
nogo(
|
||||
name = "nogo",
|
||||
config = ".bazel/nogo-config.json",
|
||||
vet = True,
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["zot.go"],
|
||||
importpath = "github.com/anuvu/zot",
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "exported_testdata",
|
||||
srcs = glob([
|
||||
"test/data/*",
|
||||
]),
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
36
Makefile
Normal file
36
Makefile
Normal file
|
@ -0,0 +1,36 @@
|
|||
export GO111MODULE=on
|
||||
|
||||
.PHONY: all
|
||||
all: doc binary debug test check
|
||||
|
||||
.PHONY: binary
|
||||
binary: doc
|
||||
go build -v -o bin/zot -tags=jsoniter ./cmd/zot
|
||||
|
||||
.PHONY: debug
|
||||
debug: doc
|
||||
go build -v -gcflags '-N -l' -o bin/zot-debug -tags=jsoniter ./cmd/zot
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
go test -v -race -cover ./pkg/... ./cmd/...
|
||||
|
||||
./bin/golangci-lint:
|
||||
curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s v1.17.1
|
||||
|
||||
.PHONY: check
|
||||
check: ./bin/golangci-lint
|
||||
./bin/golangci-lint run --enable-all ./cmd/... ./pkg/...
|
||||
|
||||
.PHONY: doc
|
||||
doc:
|
||||
swag -v || go get -u github.com/swaggo/swag/cmd/swag
|
||||
swag init -g pkg/api/routes.go
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -f bin/zot*
|
||||
|
||||
.PHONY: run
|
||||
run: binary test
|
||||
./bin/zot serve examples/config-test.json
|
58
Makefile.bazel
Normal file
58
Makefile.bazel
Normal file
|
@ -0,0 +1,58 @@
|
|||
OS := $(shell uname -s | tr A-Z a-z)
|
||||
BINARY_PREFIX_DIRECTORY := $(OS)_amd64_stripped
|
||||
|
||||
.PHONY: all
|
||||
all: build check
|
||||
|
||||
.PHONY: info
|
||||
.SILENT: info
|
||||
info:
|
||||
bazel build @io_bazel_rules_go//:go_info
|
||||
cat bazel-bin/external/io_bazel_rules_go/linux_amd64_stripped/go_info%/go_info_report
|
||||
|
||||
.PHONY: setup-base
|
||||
setup-base:
|
||||
swag -v || go get -u github.com/swaggo/swag/cmd/swag
|
||||
swag init -g pkg/api/routes.go
|
||||
|
||||
.PHONY: fmt-bazel
|
||||
fmt-bazel:
|
||||
bazel run //:buildozer
|
||||
bazel run //:buildifier
|
||||
|
||||
.PHONY: update-bazel
|
||||
update-bazel:
|
||||
bazel run //:gazelle
|
||||
bazel run //:gazelle -- update-repos -from_file=go.mod
|
||||
|
||||
.PHONY: init
|
||||
init: setup-base update-bazel fmt-bazel
|
||||
|
||||
.PHONY: build
|
||||
build:
|
||||
bazel build //...
|
||||
bazel test //...
|
||||
|
||||
.PHONY: check
|
||||
check:
|
||||
bazel run //:golangcilint
|
||||
|
||||
.PHONY: bench
|
||||
.SILENT: bench
|
||||
bench:
|
||||
for i in $$(bazel query 'tests(//...)'); do \
|
||||
bazel run $$i -- -test.bench=.; \
|
||||
done
|
||||
|
||||
.PHONY: coverage
|
||||
.SILENT: coverage
|
||||
coverage:
|
||||
bazel coverage //...
|
||||
for c in $$(find ./bazel-out/ -name 'coverage.dat'); do \
|
||||
go tool cover --html=$$c -o /tmp/cover.html; \
|
||||
cat /tmp/cover.html | grep 'option value="file' | sed 's/<[^>]*>//g' | sed 's/^[ \t]*//'; \
|
||||
done
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
bazel clean
|
12
README.md
12
README.md
|
@ -1,2 +1,10 @@
|
|||
# zot
|
||||
zot - A single-purpose OCI image repository server based on OCI Distribution Specification.
|
||||
# zot [![Build Status](https://travis-ci.org/anuvu/zot.svg?branch=master)](https://travis-ci.org/anuvu/zot)
|
||||
|
||||
**zot** is a single-purpose OCI image repository server based on the
|
||||
[OCI distribution spec](https://github.com/opencontainers/distribution-spec).
|
||||
|
||||
* Conforms to [OCI distribution spec](https://github.com/opencontainers/distribution-spec) APIs
|
||||
* Uses [OCI storage layout](https://github.com/opencontainers/image-spec/blob/master/image-layout.md) for storage layout
|
||||
* TLS support
|
||||
* *Basic* and TLS mutual authentication
|
||||
* Swagger based documentation
|
||||
|
|
832
WORKSPACE
Normal file
832
WORKSPACE
Normal file
|
@ -0,0 +1,832 @@
|
|||
workspace(name = "com_github_anuvu_zot")
|
||||
|
||||
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
|
||||
load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")
|
||||
|
||||
go_rules_version = "0.18.6"
|
||||
|
||||
http_archive(
|
||||
name = "io_bazel_rules_go",
|
||||
sha256 = "f04d2373bcaf8aa09bccb08a98a57e721306c8f6043a2a0ee610fd6853dcde3d",
|
||||
urls = ["https://github.com/bazelbuild/rules_go/releases/download/{}/rules_go-{}.tar.gz".format(go_rules_version, go_rules_version)],
|
||||
)
|
||||
|
||||
gazelle_version = "0.17.0"
|
||||
|
||||
http_archive(
|
||||
name = "bazel_gazelle",
|
||||
sha256 = "3c681998538231a2d24d0c07ed5a7658cb72bfb5fd4bf9911157c0e9ac6a2687",
|
||||
urls = ["https://github.com/bazelbuild/bazel-gazelle/releases/download/{}/bazel-gazelle-{}.tar.gz".format(gazelle_version, gazelle_version)],
|
||||
)
|
||||
|
||||
buildtools_version = "0.26.0"
|
||||
|
||||
http_archive(
|
||||
name = "com_github_bazelbuild_buildtools",
|
||||
sha256 = "86592d703ecbe0c5cbb5139333a63268cf58d7efd2c459c8be8e69e77d135e29",
|
||||
strip_prefix = "buildtools-{}".format(buildtools_version),
|
||||
urls = ["https://github.com/bazelbuild/buildtools/archive/{}.tar.gz".format(buildtools_version)],
|
||||
)
|
||||
|
||||
git_repository(
|
||||
name = "com_github_atlassian_bazel_tools",
|
||||
commit = "6fbc36c639a8f376182bb0057dd557eb2440d4ed",
|
||||
remote = "https://github.com/atlassian/bazel-tools.git",
|
||||
)
|
||||
|
||||
skylib_version = "0.8.0"
|
||||
|
||||
http_archive(
|
||||
name = "bazel_skylib",
|
||||
sha256 = "2ea8a5ed2b448baf4a6855d3ce049c4c452a6470b1efd1504fdb7c1c134d220a",
|
||||
strip_prefix = "bazel-skylib-{}".format(skylib_version),
|
||||
urls = ["https://github.com/bazelbuild/bazel-skylib/archive/{}.tar.gz".format(skylib_version)],
|
||||
)
|
||||
|
||||
load("@bazel_skylib//lib:versions.bzl", "versions")
|
||||
|
||||
versions.check(minimum_bazel_version = "0.26.1")
|
||||
|
||||
load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains", "go_rules_dependencies")
|
||||
|
||||
go_rules_dependencies()
|
||||
|
||||
go_register_toolchains(nogo = "@//:nogo")
|
||||
|
||||
load("@com_github_bazelbuild_buildtools//buildifier:deps.bzl", "buildifier_dependencies")
|
||||
load("@com_github_atlassian_bazel_tools//buildozer:deps.bzl", "buildozer_dependencies")
|
||||
load("@com_github_atlassian_bazel_tools//goimports:deps.bzl", "goimports_dependencies")
|
||||
load("@com_github_atlassian_bazel_tools//golangcilint:deps.bzl", "golangcilint_dependencies")
|
||||
load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies", "go_repository")
|
||||
|
||||
gazelle_dependencies()
|
||||
|
||||
goimports_dependencies()
|
||||
|
||||
buildifier_dependencies()
|
||||
|
||||
buildozer_dependencies()
|
||||
|
||||
golangcilint_dependencies()
|
||||
|
||||
go_repository(
|
||||
name = "co_honnef_go_tools",
|
||||
commit = "c2f93a96b099",
|
||||
importpath = "honnef.co/go/tools",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_alecthomas_template",
|
||||
commit = "a0175ee3bccc",
|
||||
importpath = "github.com/alecthomas/template",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_alecthomas_units",
|
||||
commit = "2efee857e7cf",
|
||||
importpath = "github.com/alecthomas/units",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_armon_consul_api",
|
||||
commit = "eb2c6b5be1b6",
|
||||
importpath = "github.com/armon/consul-api",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_beorn7_perks",
|
||||
importpath = "github.com/beorn7/perks",
|
||||
tag = "v1.0.0",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_burntsushi_toml",
|
||||
importpath = "github.com/BurntSushi/toml",
|
||||
tag = "v0.3.1",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_cespare_xxhash",
|
||||
importpath = "github.com/cespare/xxhash",
|
||||
tag = "v1.1.0",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_client9_misspell",
|
||||
importpath = "github.com/client9/misspell",
|
||||
tag = "v0.3.4",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_coreos_bbolt",
|
||||
importpath = "github.com/coreos/bbolt",
|
||||
tag = "v1.3.2",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_coreos_etcd",
|
||||
importpath = "github.com/coreos/etcd",
|
||||
tag = "v3.3.10",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_coreos_go_semver",
|
||||
importpath = "github.com/coreos/go-semver",
|
||||
tag = "v0.2.0",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_coreos_go_systemd",
|
||||
commit = "95778dfbb74e",
|
||||
importpath = "github.com/coreos/go-systemd",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_coreos_pkg",
|
||||
commit = "399ea9e2e55f",
|
||||
importpath = "github.com/coreos/pkg",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_cpuguy83_go_md2man",
|
||||
importpath = "github.com/cpuguy83/go-md2man",
|
||||
tag = "v1.0.10",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_davecgh_go_spew",
|
||||
importpath = "github.com/davecgh/go-spew",
|
||||
tag = "v1.1.1",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_dgrijalva_jwt_go",
|
||||
importpath = "github.com/dgrijalva/jwt-go",
|
||||
tag = "v3.2.0",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_dgryski_go_sip13",
|
||||
commit = "e10d5fee7954",
|
||||
importpath = "github.com/dgryski/go-sip13",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_fsnotify_fsnotify",
|
||||
importpath = "github.com/fsnotify/fsnotify",
|
||||
tag = "v1.4.7",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_ghodss_yaml",
|
||||
importpath = "github.com/ghodss/yaml",
|
||||
tag = "v1.0.0",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_gin_contrib_sse",
|
||||
commit = "5545eab6dad3",
|
||||
importpath = "github.com/gin-contrib/sse",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_gin_gonic_gin",
|
||||
importpath = "github.com/gin-gonic/gin",
|
||||
tag = "v1.4.0",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_go_kit_kit",
|
||||
importpath = "github.com/go-kit/kit",
|
||||
tag = "v0.8.0",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_go_logfmt_logfmt",
|
||||
importpath = "github.com/go-logfmt/logfmt",
|
||||
tag = "v0.4.0",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_go_openapi_jsonpointer",
|
||||
importpath = "github.com/go-openapi/jsonpointer",
|
||||
tag = "v0.17.0",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_go_openapi_jsonreference",
|
||||
importpath = "github.com/go-openapi/jsonreference",
|
||||
tag = "v0.19.0",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_go_openapi_spec",
|
||||
importpath = "github.com/go-openapi/spec",
|
||||
tag = "v0.19.0",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_go_openapi_swag",
|
||||
importpath = "github.com/go-openapi/swag",
|
||||
tag = "v0.17.0",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_go_stack_stack",
|
||||
importpath = "github.com/go-stack/stack",
|
||||
tag = "v1.8.0",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_gofrs_uuid",
|
||||
importpath = "github.com/gofrs/uuid",
|
||||
tag = "v3.2.0",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_gogo_protobuf",
|
||||
importpath = "github.com/gogo/protobuf",
|
||||
tag = "v1.2.1",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_golang_glog",
|
||||
commit = "23def4e6c14b",
|
||||
importpath = "github.com/golang/glog",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_golang_groupcache",
|
||||
commit = "5b532d6fd5ef",
|
||||
importpath = "github.com/golang/groupcache",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_golang_mock",
|
||||
importpath = "github.com/golang/mock",
|
||||
tag = "v1.1.1",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_golang_protobuf",
|
||||
importpath = "github.com/golang/protobuf",
|
||||
tag = "v1.3.1",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_google_btree",
|
||||
importpath = "github.com/google/btree",
|
||||
tag = "v1.0.0",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_google_go_cmp",
|
||||
importpath = "github.com/google/go-cmp",
|
||||
tag = "v0.2.0",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_gopherjs_gopherjs",
|
||||
commit = "0766667cb4d1",
|
||||
importpath = "github.com/gopherjs/gopherjs",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_gorilla_websocket",
|
||||
importpath = "github.com/gorilla/websocket",
|
||||
tag = "v1.4.0",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_grpc_ecosystem_go_grpc_middleware",
|
||||
importpath = "github.com/grpc-ecosystem/go-grpc-middleware",
|
||||
tag = "v1.0.0",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_grpc_ecosystem_go_grpc_prometheus",
|
||||
importpath = "github.com/grpc-ecosystem/go-grpc-prometheus",
|
||||
tag = "v1.2.0",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_grpc_ecosystem_grpc_gateway",
|
||||
importpath = "github.com/grpc-ecosystem/grpc-gateway",
|
||||
tag = "v1.9.0",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_hashicorp_hcl",
|
||||
importpath = "github.com/hashicorp/hcl",
|
||||
tag = "v1.0.0",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_inconshreveable_mousetrap",
|
||||
importpath = "github.com/inconshreveable/mousetrap",
|
||||
tag = "v1.0.0",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_jonboulle_clockwork",
|
||||
importpath = "github.com/jonboulle/clockwork",
|
||||
tag = "v0.1.0",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_json_iterator_go",
|
||||
importpath = "github.com/json-iterator/go",
|
||||
tag = "v1.1.6",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_jtolds_gls",
|
||||
importpath = "github.com/jtolds/gls",
|
||||
tag = "v4.20.0",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_julienschmidt_httprouter",
|
||||
importpath = "github.com/julienschmidt/httprouter",
|
||||
tag = "v1.2.0",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_kisielk_errcheck",
|
||||
importpath = "github.com/kisielk/errcheck",
|
||||
tag = "v1.1.0",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_kisielk_gotool",
|
||||
importpath = "github.com/kisielk/gotool",
|
||||
tag = "v1.0.0",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_konsorten_go_windows_terminal_sequences",
|
||||
importpath = "github.com/konsorten/go-windows-terminal-sequences",
|
||||
tag = "v1.0.1",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_kr_logfmt",
|
||||
commit = "b84e30acd515",
|
||||
importpath = "github.com/kr/logfmt",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_kr_pretty",
|
||||
importpath = "github.com/kr/pretty",
|
||||
tag = "v0.1.0",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_kr_pty",
|
||||
importpath = "github.com/kr/pty",
|
||||
tag = "v1.1.1",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_kr_text",
|
||||
importpath = "github.com/kr/text",
|
||||
tag = "v0.1.0",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_magiconair_properties",
|
||||
importpath = "github.com/magiconair/properties",
|
||||
tag = "v1.8.0",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_mailru_easyjson",
|
||||
commit = "60711f1a8329",
|
||||
importpath = "github.com/mailru/easyjson",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_mattn_go_isatty",
|
||||
importpath = "github.com/mattn/go-isatty",
|
||||
tag = "v0.0.7",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_matttproud_golang_protobuf_extensions",
|
||||
importpath = "github.com/matttproud/golang_protobuf_extensions",
|
||||
tag = "v1.0.1",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_mitchellh_go_homedir",
|
||||
importpath = "github.com/mitchellh/go-homedir",
|
||||
tag = "v1.1.0",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_mitchellh_mapstructure",
|
||||
importpath = "github.com/mitchellh/mapstructure",
|
||||
tag = "v1.1.2",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_modern_go_concurrent",
|
||||
commit = "bacd9c7ef1dd",
|
||||
importpath = "github.com/modern-go/concurrent",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_modern_go_reflect2",
|
||||
importpath = "github.com/modern-go/reflect2",
|
||||
tag = "v1.0.1",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_mwitkow_go_conntrack",
|
||||
commit = "cc309e4a2223",
|
||||
importpath = "github.com/mwitkow/go-conntrack",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_oklog_ulid",
|
||||
importpath = "github.com/oklog/ulid",
|
||||
tag = "v1.3.1",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_oneofone_xxhash",
|
||||
importpath = "github.com/OneOfOne/xxhash",
|
||||
tag = "v1.2.2",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_opencontainers_distribution_spec",
|
||||
importpath = "github.com/opencontainers/distribution-spec",
|
||||
tag = "v1.0.0-rc0",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_opencontainers_go_digest",
|
||||
importpath = "github.com/opencontainers/go-digest",
|
||||
tag = "v1.0.0-rc1",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_opencontainers_image_spec",
|
||||
importpath = "github.com/opencontainers/image-spec",
|
||||
tag = "v1.0.1",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_pelletier_go_toml",
|
||||
importpath = "github.com/pelletier/go-toml",
|
||||
tag = "v1.2.0",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_pkg_errors",
|
||||
importpath = "github.com/pkg/errors",
|
||||
tag = "v0.8.1",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_pmezard_go_difflib",
|
||||
importpath = "github.com/pmezard/go-difflib",
|
||||
tag = "v1.0.0",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_prometheus_client_golang",
|
||||
importpath = "github.com/prometheus/client_golang",
|
||||
tag = "v0.9.3",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_prometheus_client_model",
|
||||
commit = "fd36f4220a90",
|
||||
importpath = "github.com/prometheus/client_model",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_prometheus_common",
|
||||
importpath = "github.com/prometheus/common",
|
||||
tag = "v0.4.0",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_prometheus_procfs",
|
||||
commit = "5867b95ac084",
|
||||
importpath = "github.com/prometheus/procfs",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_prometheus_tsdb",
|
||||
importpath = "github.com/prometheus/tsdb",
|
||||
tag = "v0.7.1",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_puerkitobio_purell",
|
||||
importpath = "github.com/PuerkitoBio/purell",
|
||||
tag = "v1.1.0",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_puerkitobio_urlesc",
|
||||
commit = "de5bf2ad4578",
|
||||
importpath = "github.com/PuerkitoBio/urlesc",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_rogpeppe_fastuuid",
|
||||
commit = "6724a57986af",
|
||||
importpath = "github.com/rogpeppe/fastuuid",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_rs_xid",
|
||||
importpath = "github.com/rs/xid",
|
||||
tag = "v1.2.1",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_rs_zerolog",
|
||||
importpath = "github.com/rs/zerolog",
|
||||
tag = "v1.14.3",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_russross_blackfriday",
|
||||
importpath = "github.com/russross/blackfriday",
|
||||
tag = "v1.5.2",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_sirupsen_logrus",
|
||||
importpath = "github.com/sirupsen/logrus",
|
||||
tag = "v1.2.0",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_smartystreets_assertions",
|
||||
commit = "b2de0cb4f26d",
|
||||
importpath = "github.com/smartystreets/assertions",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_smartystreets_goconvey",
|
||||
commit = "68dc04aab96a",
|
||||
importpath = "github.com/smartystreets/goconvey",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_soheilhy_cmux",
|
||||
importpath = "github.com/soheilhy/cmux",
|
||||
tag = "v0.1.4",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_spaolacci_murmur3",
|
||||
commit = "f09979ecbc72",
|
||||
importpath = "github.com/spaolacci/murmur3",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_spf13_afero",
|
||||
importpath = "github.com/spf13/afero",
|
||||
tag = "v1.1.2",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_spf13_cast",
|
||||
importpath = "github.com/spf13/cast",
|
||||
tag = "v1.3.0",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_spf13_cobra",
|
||||
importpath = "github.com/spf13/cobra",
|
||||
tag = "v0.0.5",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_spf13_jwalterweatherman",
|
||||
importpath = "github.com/spf13/jwalterweatherman",
|
||||
tag = "v1.0.0",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_spf13_pflag",
|
||||
importpath = "github.com/spf13/pflag",
|
||||
tag = "v1.0.3",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_spf13_viper",
|
||||
importpath = "github.com/spf13/viper",
|
||||
tag = "v1.4.0",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_stretchr_objx",
|
||||
importpath = "github.com/stretchr/objx",
|
||||
tag = "v0.1.1",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_stretchr_testify",
|
||||
importpath = "github.com/stretchr/testify",
|
||||
tag = "v1.3.0",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_swaggo_gin_swagger",
|
||||
importpath = "github.com/swaggo/gin-swagger",
|
||||
tag = "v1.1.0",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_swaggo_swag",
|
||||
importpath = "github.com/swaggo/swag",
|
||||
tag = "v1.5.1",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_tmc_grpc_websocket_proxy",
|
||||
commit = "0ad062ec5ee5",
|
||||
importpath = "github.com/tmc/grpc-websocket-proxy",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_ugorji_go",
|
||||
importpath = "github.com/ugorji/go",
|
||||
tag = "v1.1.5-pre",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_ugorji_go_codec",
|
||||
importpath = "github.com/ugorji/go/codec",
|
||||
tag = "v1.1.5-pre",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_urfave_cli",
|
||||
importpath = "github.com/urfave/cli",
|
||||
tag = "v1.20.0",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_xiang90_probing",
|
||||
commit = "43a291ad63a2",
|
||||
importpath = "github.com/xiang90/probing",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_xordataexchange_crypt",
|
||||
commit = "b2862e3d0a77",
|
||||
importpath = "github.com/xordataexchange/crypt",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_github_zenazn_goji",
|
||||
importpath = "github.com/zenazn/goji",
|
||||
tag = "v0.9.0",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "com_google_cloud_go",
|
||||
importpath = "cloud.google.com/go",
|
||||
tag = "v0.26.0",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "in_gopkg_alecthomas_kingpin_v2",
|
||||
importpath = "gopkg.in/alecthomas/kingpin.v2",
|
||||
tag = "v2.2.6",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "in_gopkg_check_v1",
|
||||
commit = "788fd7840127",
|
||||
importpath = "gopkg.in/check.v1",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "in_gopkg_go_playground_assert_v1",
|
||||
importpath = "gopkg.in/go-playground/assert.v1",
|
||||
tag = "v1.2.1",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "in_gopkg_go_playground_validator_v8",
|
||||
importpath = "gopkg.in/go-playground/validator.v8",
|
||||
tag = "v8.18.2",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "in_gopkg_resty_v1",
|
||||
importpath = "gopkg.in/resty.v1",
|
||||
tag = "v1.12.0",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "in_gopkg_yaml_v2",
|
||||
importpath = "gopkg.in/yaml.v2",
|
||||
tag = "v2.2.2",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "io_etcd_go_bbolt",
|
||||
importpath = "go.etcd.io/bbolt",
|
||||
tag = "v1.3.2",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "org_golang_google_appengine",
|
||||
importpath = "google.golang.org/appengine",
|
||||
tag = "v1.1.0",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "org_golang_google_genproto",
|
||||
commit = "c66870c02cf8",
|
||||
importpath = "google.golang.org/genproto",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "org_golang_google_grpc",
|
||||
importpath = "google.golang.org/grpc",
|
||||
tag = "v1.21.0",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "org_golang_x_crypto",
|
||||
commit = "ea8f1a30c443",
|
||||
importpath = "golang.org/x/crypto",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "org_golang_x_lint",
|
||||
commit = "d0100b6bd8b3",
|
||||
importpath = "golang.org/x/lint",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "org_golang_x_net",
|
||||
commit = "f3200d17e092",
|
||||
importpath = "golang.org/x/net",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "org_golang_x_oauth2",
|
||||
commit = "d2e6202438be",
|
||||
importpath = "golang.org/x/oauth2",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "org_golang_x_sync",
|
||||
commit = "112230192c58",
|
||||
importpath = "golang.org/x/sync",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "org_golang_x_sys",
|
||||
commit = "97732733099d",
|
||||
importpath = "golang.org/x/sys",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "org_golang_x_text",
|
||||
importpath = "golang.org/x/text",
|
||||
tag = "v0.3.0",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "org_golang_x_time",
|
||||
commit = "9d24e82272b4",
|
||||
importpath = "golang.org/x/time",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "org_golang_x_tools",
|
||||
commit = "4d9ae51c2468",
|
||||
importpath = "golang.org/x/tools",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "org_uber_go_atomic",
|
||||
importpath = "go.uber.org/atomic",
|
||||
tag = "v1.4.0",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "org_uber_go_multierr",
|
||||
importpath = "go.uber.org/multierr",
|
||||
tag = "v1.1.0",
|
||||
)
|
||||
|
||||
go_repository(
|
||||
name = "org_uber_go_zap",
|
||||
importpath = "go.uber.org/zap",
|
||||
tag = "v1.10.0",
|
||||
)
|
28
cmd/zot/BUILD.bazel
Normal file
28
cmd/zot/BUILD.bazel
Normal file
|
@ -0,0 +1,28 @@
|
|||
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library", "go_test")
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["main.go"],
|
||||
importpath = "github.com/anuvu/zot/cmd/zot",
|
||||
visibility = ["//visibility:private"],
|
||||
deps = ["//pkg/cli:go_default_library"],
|
||||
)
|
||||
|
||||
go_binary(
|
||||
name = "zot",
|
||||
embed = [":go_default_library"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
timeout = "short",
|
||||
srcs = ["main_test.go"],
|
||||
embed = [":go_default_library"],
|
||||
race = "on",
|
||||
deps = [
|
||||
"//pkg/api:go_default_library",
|
||||
"//pkg/cli:go_default_library",
|
||||
"@com_github_smartystreets_goconvey//convey:go_default_library",
|
||||
],
|
||||
)
|
13
cmd/zot/main.go
Normal file
13
cmd/zot/main.go
Normal file
|
@ -0,0 +1,13 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/anuvu/zot/pkg/cli"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := cli.NewRootCmd().Execute(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
22
cmd/zot/main_test.go
Normal file
22
cmd/zot/main_test.go
Normal file
|
@ -0,0 +1,22 @@
|
|||
package main_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/anuvu/zot/pkg/api"
|
||||
"github.com/anuvu/zot/pkg/cli"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestIntegration(t *testing.T) {
|
||||
Convey("Make a new controller", t, func() {
|
||||
config := api.NewConfig()
|
||||
c := api.NewController(config)
|
||||
So(c, ShouldNotBeNil)
|
||||
|
||||
cl := cli.NewRootCmd()
|
||||
So(cl, ShouldNotBeNil)
|
||||
|
||||
So(cl.Execute(), ShouldBeNil)
|
||||
})
|
||||
}
|
12
docs/BUILD.bazel
Normal file
12
docs/BUILD.bazel
Normal file
|
@ -0,0 +1,12 @@
|
|||
load("@io_bazel_rules_go//go:def.bzl", "go_library")
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["docs.go"],
|
||||
importpath = "github.com/anuvu/zot/docs",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"@com_github_alecthomas_template//:go_default_library",
|
||||
"@com_github_swaggo_swag//:go_default_library",
|
||||
],
|
||||
)
|
749
docs/docs.go
Normal file
749
docs/docs.go
Normal file
|
@ -0,0 +1,749 @@
|
|||
// 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
|
||||
|
||||
package docs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"github.com/alecthomas/template"
|
||||
"github.com/swaggo/swag"
|
||||
)
|
||||
|
||||
var doc = `{
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"description": "APIs for Open Container Initiative Distribution Specification",
|
||||
"title": "Open Container Initiative Distribution Specification",
|
||||
"contact": {
|
||||
"name": "API Support",
|
||||
"url": "http://www.swagger.io/support",
|
||||
"email": "support@swagger.io"
|
||||
},
|
||||
"license": {
|
||||
"name": "Apache 2.0",
|
||||
"url": "http://www.apache.org/licenses/LICENSE-2.0.html"
|
||||
},
|
||||
"version": "v0.1.0-dev"
|
||||
},
|
||||
"host": "{{.Host}}",
|
||||
"basePath": "{{.BasePath}}",
|
||||
"paths": {
|
||||
"/v2/": {
|
||||
"get": {
|
||||
"description": "Check if this API version is supported",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"summary": "Check API support",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "ok",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v2/_catalog": {
|
||||
"get": {
|
||||
"description": "List all image repositories",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"summary": "List image repositories",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/api.RepositoryList"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "internal server error",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v2/{name}/blobs/uploads": {
|
||||
"post": {
|
||||
"description": "Create a new image blob/layer upload",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"summary": "Create image blob/layer upload",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "repository name",
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"202": {
|
||||
"description": "accepted",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"headers": {
|
||||
"Location": {
|
||||
"type": "string",
|
||||
"description": "/v2/{name}/blobs/uploads/{uuid}"
|
||||
},
|
||||
"Range": {
|
||||
"type": "string",
|
||||
"description": "bytes=0-0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "not found",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "internal server error",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v2/{name}/blobs/uploads/{uuid}": {
|
||||
"get": {
|
||||
"description": "Get an image's blob/layer upload given a uuid",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"summary": "Get image blob/layer upload",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "repository name",
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "upload uuid",
|
||||
"name": "uuid",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "no content",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "not found",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "internal server error",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"description": "Update and finish an image's blob/layer upload given a digest",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"summary": "Update image blob/layer upload",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "repository name",
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "upload uuid",
|
||||
"name": "uuid",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "blob/layer digest",
|
||||
"name": "digest",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "created",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "not found",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "internal server error",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"description": "Delete an image's blob/layer given a digest",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"summary": "Delete image blob/layer",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "repository name",
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "upload uuid",
|
||||
"name": "uuid",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "ok",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "not found",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "internal server error",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"patch": {
|
||||
"description": "Resume an image's blob/layer upload given an uuid",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"summary": "Resume image blob/layer upload",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "repository name",
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "upload uuid",
|
||||
"name": "uuid",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"202": {
|
||||
"description": "accepted",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"headers": {
|
||||
"Location": {
|
||||
"type": "string",
|
||||
"description": "/v2/{name}/blobs/uploads/{uuid}"
|
||||
},
|
||||
"Range": {
|
||||
"type": "string",
|
||||
"description": "bytes=0-128"
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "bad request",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "not found",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"416": {
|
||||
"description": "range not satisfiable",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "internal server error",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v2/{name}/blobs/{digest}": {
|
||||
"get": {
|
||||
"description": "Get an image's blob/layer given a digest",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/vnd.oci.image.layer.v1.tar+gzip"
|
||||
],
|
||||
"summary": "Get image blob/layer",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "repository name",
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "blob/layer digest",
|
||||
"name": "digest",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/api.ImageManifest"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"description": "Delete an image's blob/layer given a digest",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"summary": "Delete image blob/layer",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "repository name",
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "blob/layer digest",
|
||||
"name": "digest",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"202": {
|
||||
"description": "accepted",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"head": {
|
||||
"description": "Check an image's blob/layer given a digest",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"summary": "Check image blob/layer",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "repository name",
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "blob/layer digest",
|
||||
"name": "digest",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/api.ImageManifest"
|
||||
},
|
||||
"headers": {
|
||||
"api.DistContentDigestKey": {
|
||||
"type": "object",
|
||||
"description": "OK"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v2/{name}/manifests/{reference}": {
|
||||
"get": {
|
||||
"description": "Get an image's manifest given a reference or a digest",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/vnd.oci.image.manifest.v1+json"
|
||||
],
|
||||
"summary": "Get image manifest",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "repository name",
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "image reference or digest",
|
||||
"name": "reference",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/api.ImageManifest"
|
||||
},
|
||||
"headers": {
|
||||
"api.DistContentDigestKey": {
|
||||
"type": "object",
|
||||
"description": "OK"
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "not found",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "internal server error",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"description": "Update an image's manifest given a reference or a digest",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"summary": "Update image manifest",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "repository name",
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "image reference or digest",
|
||||
"name": "reference",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "created",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "bad request",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "not found",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "internal server error",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"description": "Delete an image's manifest given a reference or a digest",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"summary": "Delete image manifest",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "repository name",
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "image reference or digest",
|
||||
"name": "reference",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "ok",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"head": {
|
||||
"description": "Check an image's manifest given a reference or a digest",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"summary": "Check image manifest",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "repository name",
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "image reference or digest",
|
||||
"name": "reference",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "ok",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"headers": {
|
||||
"api.DistContentDigestKey": {
|
||||
"type": "object",
|
||||
"description": "OK"
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "not found",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "internal server error",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v2/{name}/tags/list": {
|
||||
"get": {
|
||||
"description": "List all image tags in a repository",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"summary": "List image tags",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "test",
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/api.ImageTags"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "not found",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"api.ImageManifest": {
|
||||
"type": "object"
|
||||
},
|
||||
"api.ImageTags": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"api.RepositoryList": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"repositories": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
type swaggerInfo struct {
|
||||
Version string
|
||||
Host string
|
||||
BasePath string
|
||||
Title string
|
||||
Description string
|
||||
}
|
||||
|
||||
// SwaggerInfo holds exported Swagger Info so clients can modify it
|
||||
var SwaggerInfo swaggerInfo
|
||||
|
||||
type s struct{}
|
||||
|
||||
func (s *s) ReadDoc() string {
|
||||
t, err := template.New("swagger_info").Parse(doc)
|
||||
if err != nil {
|
||||
return doc
|
||||
}
|
||||
|
||||
var tpl bytes.Buffer
|
||||
if err := t.Execute(&tpl, SwaggerInfo); err != nil {
|
||||
return doc
|
||||
}
|
||||
|
||||
return tpl.String()
|
||||
}
|
||||
|
||||
func init() {
|
||||
swag.Register(swag.Name, &s{})
|
||||
}
|
705
docs/swagger.json
Normal file
705
docs/swagger.json
Normal file
|
@ -0,0 +1,705 @@
|
|||
{
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"description": "APIs for Open Container Initiative Distribution Specification",
|
||||
"title": "Open Container Initiative Distribution Specification",
|
||||
"contact": {
|
||||
"name": "API Support",
|
||||
"url": "http://www.swagger.io/support",
|
||||
"email": "support@swagger.io"
|
||||
},
|
||||
"license": {
|
||||
"name": "Apache 2.0",
|
||||
"url": "http://www.apache.org/licenses/LICENSE-2.0.html"
|
||||
},
|
||||
"version": "v0.1.0-dev"
|
||||
},
|
||||
"host": "{{.Host}}",
|
||||
"basePath": "{{.BasePath}}",
|
||||
"paths": {
|
||||
"/v2/": {
|
||||
"get": {
|
||||
"description": "Check if this API version is supported",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"summary": "Check API support",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "ok",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v2/_catalog": {
|
||||
"get": {
|
||||
"description": "List all image repositories",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"summary": "List image repositories",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/api.RepositoryList"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "internal server error",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v2/{name}/blobs/uploads": {
|
||||
"post": {
|
||||
"description": "Create a new image blob/layer upload",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"summary": "Create image blob/layer upload",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "repository name",
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"202": {
|
||||
"description": "accepted",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"headers": {
|
||||
"Location": {
|
||||
"type": "string",
|
||||
"description": "/v2/{name}/blobs/uploads/{uuid}"
|
||||
},
|
||||
"Range": {
|
||||
"type": "string",
|
||||
"description": "bytes=0-0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "not found",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "internal server error",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v2/{name}/blobs/uploads/{uuid}": {
|
||||
"get": {
|
||||
"description": "Get an image's blob/layer upload given a uuid",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"summary": "Get image blob/layer upload",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "repository name",
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "upload uuid",
|
||||
"name": "uuid",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "no content",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "not found",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "internal server error",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"description": "Update and finish an image's blob/layer upload given a digest",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"summary": "Update image blob/layer upload",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "repository name",
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "upload uuid",
|
||||
"name": "uuid",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "blob/layer digest",
|
||||
"name": "digest",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "created",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "not found",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "internal server error",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"description": "Delete an image's blob/layer given a digest",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"summary": "Delete image blob/layer",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "repository name",
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "upload uuid",
|
||||
"name": "uuid",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "ok",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "not found",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "internal server error",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"patch": {
|
||||
"description": "Resume an image's blob/layer upload given an uuid",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"summary": "Resume image blob/layer upload",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "repository name",
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "upload uuid",
|
||||
"name": "uuid",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"202": {
|
||||
"description": "accepted",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"headers": {
|
||||
"Location": {
|
||||
"type": "string",
|
||||
"description": "/v2/{name}/blobs/uploads/{uuid}"
|
||||
},
|
||||
"Range": {
|
||||
"type": "string",
|
||||
"description": "bytes=0-128"
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "bad request",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "not found",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"416": {
|
||||
"description": "range not satisfiable",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "internal server error",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v2/{name}/blobs/{digest}": {
|
||||
"get": {
|
||||
"description": "Get an image's blob/layer given a digest",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/vnd.oci.image.layer.v1.tar+gzip"
|
||||
],
|
||||
"summary": "Get image blob/layer",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "repository name",
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "blob/layer digest",
|
||||
"name": "digest",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/api.ImageManifest"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"description": "Delete an image's blob/layer given a digest",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"summary": "Delete image blob/layer",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "repository name",
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "blob/layer digest",
|
||||
"name": "digest",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"202": {
|
||||
"description": "accepted",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"head": {
|
||||
"description": "Check an image's blob/layer given a digest",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"summary": "Check image blob/layer",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "repository name",
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "blob/layer digest",
|
||||
"name": "digest",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/api.ImageManifest"
|
||||
},
|
||||
"headers": {
|
||||
"api.DistContentDigestKey": {
|
||||
"type": "object",
|
||||
"description": "OK"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v2/{name}/manifests/{reference}": {
|
||||
"get": {
|
||||
"description": "Get an image's manifest given a reference or a digest",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/vnd.oci.image.manifest.v1+json"
|
||||
],
|
||||
"summary": "Get image manifest",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "repository name",
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "image reference or digest",
|
||||
"name": "reference",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/api.ImageManifest"
|
||||
},
|
||||
"headers": {
|
||||
"api.DistContentDigestKey": {
|
||||
"type": "object",
|
||||
"description": "OK"
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "not found",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "internal server error",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"description": "Update an image's manifest given a reference or a digest",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"summary": "Update image manifest",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "repository name",
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "image reference or digest",
|
||||
"name": "reference",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "created",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "bad request",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "not found",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "internal server error",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"description": "Delete an image's manifest given a reference or a digest",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"summary": "Delete image manifest",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "repository name",
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "image reference or digest",
|
||||
"name": "reference",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "ok",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"head": {
|
||||
"description": "Check an image's manifest given a reference or a digest",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"summary": "Check image manifest",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "repository name",
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "image reference or digest",
|
||||
"name": "reference",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "ok",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"headers": {
|
||||
"api.DistContentDigestKey": {
|
||||
"type": "object",
|
||||
"description": "OK"
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "not found",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "internal server error",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v2/{name}/tags/list": {
|
||||
"get": {
|
||||
"description": "List all image tags in a repository",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"summary": "List image tags",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "test",
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/api.ImageTags"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "not found",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"api.ImageManifest": {
|
||||
"type": "object"
|
||||
},
|
||||
"api.ImageTags": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"api.RepositoryList": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"repositories": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
474
docs/swagger.yaml
Normal file
474
docs/swagger.yaml
Normal file
|
@ -0,0 +1,474 @@
|
|||
basePath: '{{.BasePath}}'
|
||||
definitions:
|
||||
api.ImageManifest:
|
||||
type: object
|
||||
api.ImageTags:
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
tags:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
type: object
|
||||
api.RepositoryList:
|
||||
properties:
|
||||
repositories:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
type: object
|
||||
host: '{{.Host}}'
|
||||
info:
|
||||
contact:
|
||||
email: support@swagger.io
|
||||
name: API Support
|
||||
url: http://www.swagger.io/support
|
||||
description: APIs for Open Container Initiative Distribution Specification
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
title: Open Container Initiative Distribution Specification
|
||||
version: v0.1.0-dev
|
||||
paths:
|
||||
/v2/:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Check if this API version is supported
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: ok
|
||||
schema:
|
||||
type: string
|
||||
summary: Check API support
|
||||
/v2/_catalog:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: List all image repositories
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/api.RepositoryList'
|
||||
type: object
|
||||
"500":
|
||||
description: internal server error
|
||||
schema:
|
||||
type: string
|
||||
summary: List image repositories
|
||||
/v2/{name}/blobs/{digest}:
|
||||
delete:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Delete an image's blob/layer given a digest
|
||||
parameters:
|
||||
- description: repository name
|
||||
in: path
|
||||
name: name
|
||||
required: true
|
||||
type: string
|
||||
- description: blob/layer digest
|
||||
in: path
|
||||
name: digest
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"202":
|
||||
description: accepted
|
||||
schema:
|
||||
type: string
|
||||
summary: Delete image blob/layer
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Get an image's blob/layer given a digest
|
||||
parameters:
|
||||
- description: repository name
|
||||
in: path
|
||||
name: name
|
||||
required: true
|
||||
type: string
|
||||
- description: blob/layer digest
|
||||
in: path
|
||||
name: digest
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/vnd.oci.image.layer.v1.tar+gzip
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/api.ImageManifest'
|
||||
type: object
|
||||
summary: Get image blob/layer
|
||||
head:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Check an image's blob/layer given a digest
|
||||
parameters:
|
||||
- description: repository name
|
||||
in: path
|
||||
name: name
|
||||
required: true
|
||||
type: string
|
||||
- description: blob/layer digest
|
||||
in: path
|
||||
name: digest
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
headers:
|
||||
api.DistContentDigestKey:
|
||||
description: OK
|
||||
type: object
|
||||
schema:
|
||||
$ref: '#/definitions/api.ImageManifest'
|
||||
type: object
|
||||
summary: Check image blob/layer
|
||||
/v2/{name}/blobs/uploads:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Create a new image blob/layer upload
|
||||
parameters:
|
||||
- description: repository name
|
||||
in: path
|
||||
name: name
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"202":
|
||||
description: accepted
|
||||
headers:
|
||||
Location:
|
||||
description: /v2/{name}/blobs/uploads/{uuid}
|
||||
type: string
|
||||
Range:
|
||||
description: bytes=0-0
|
||||
type: string
|
||||
schema:
|
||||
type: string
|
||||
"404":
|
||||
description: not found
|
||||
schema:
|
||||
type: string
|
||||
"500":
|
||||
description: internal server error
|
||||
schema:
|
||||
type: string
|
||||
summary: Create image blob/layer upload
|
||||
/v2/{name}/blobs/uploads/{uuid}:
|
||||
delete:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Delete an image's blob/layer given a digest
|
||||
parameters:
|
||||
- description: repository name
|
||||
in: path
|
||||
name: name
|
||||
required: true
|
||||
type: string
|
||||
- description: upload uuid
|
||||
in: path
|
||||
name: uuid
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: ok
|
||||
schema:
|
||||
type: string
|
||||
"404":
|
||||
description: not found
|
||||
schema:
|
||||
type: string
|
||||
"500":
|
||||
description: internal server error
|
||||
schema:
|
||||
type: string
|
||||
summary: Delete image blob/layer
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Get an image's blob/layer upload given a uuid
|
||||
parameters:
|
||||
- description: repository name
|
||||
in: path
|
||||
name: name
|
||||
required: true
|
||||
type: string
|
||||
- description: upload uuid
|
||||
in: path
|
||||
name: uuid
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"204":
|
||||
description: no content
|
||||
schema:
|
||||
type: string
|
||||
"404":
|
||||
description: not found
|
||||
schema:
|
||||
type: string
|
||||
"500":
|
||||
description: internal server error
|
||||
schema:
|
||||
type: string
|
||||
summary: Get image blob/layer upload
|
||||
patch:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Resume an image's blob/layer upload given an uuid
|
||||
parameters:
|
||||
- description: repository name
|
||||
in: path
|
||||
name: name
|
||||
required: true
|
||||
type: string
|
||||
- description: upload uuid
|
||||
in: path
|
||||
name: uuid
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"202":
|
||||
description: accepted
|
||||
headers:
|
||||
Location:
|
||||
description: /v2/{name}/blobs/uploads/{uuid}
|
||||
type: string
|
||||
Range:
|
||||
description: bytes=0-128
|
||||
type: string
|
||||
schema:
|
||||
type: string
|
||||
"400":
|
||||
description: bad request
|
||||
schema:
|
||||
type: string
|
||||
"404":
|
||||
description: not found
|
||||
schema:
|
||||
type: string
|
||||
"416":
|
||||
description: range not satisfiable
|
||||
schema:
|
||||
type: string
|
||||
"500":
|
||||
description: internal server error
|
||||
schema:
|
||||
type: string
|
||||
summary: Resume image blob/layer upload
|
||||
put:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Update and finish an image's blob/layer upload given a digest
|
||||
parameters:
|
||||
- description: repository name
|
||||
in: path
|
||||
name: name
|
||||
required: true
|
||||
type: string
|
||||
- description: upload uuid
|
||||
in: path
|
||||
name: uuid
|
||||
required: true
|
||||
type: string
|
||||
- description: blob/layer digest
|
||||
in: query
|
||||
name: digest
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"201":
|
||||
description: created
|
||||
schema:
|
||||
type: string
|
||||
"404":
|
||||
description: not found
|
||||
schema:
|
||||
type: string
|
||||
"500":
|
||||
description: internal server error
|
||||
schema:
|
||||
type: string
|
||||
summary: Update image blob/layer upload
|
||||
/v2/{name}/manifests/{reference}:
|
||||
delete:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Delete an image's manifest given a reference or a digest
|
||||
parameters:
|
||||
- description: repository name
|
||||
in: path
|
||||
name: name
|
||||
required: true
|
||||
type: string
|
||||
- description: image reference or digest
|
||||
in: path
|
||||
name: reference
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: ok
|
||||
schema:
|
||||
type: string
|
||||
summary: Delete image manifest
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Get an image's manifest given a reference or a digest
|
||||
parameters:
|
||||
- description: repository name
|
||||
in: path
|
||||
name: name
|
||||
required: true
|
||||
type: string
|
||||
- description: image reference or digest
|
||||
in: path
|
||||
name: reference
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/vnd.oci.image.manifest.v1+json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
headers:
|
||||
api.DistContentDigestKey:
|
||||
description: OK
|
||||
type: object
|
||||
schema:
|
||||
$ref: '#/definitions/api.ImageManifest'
|
||||
type: object
|
||||
"404":
|
||||
description: not found
|
||||
schema:
|
||||
type: string
|
||||
"500":
|
||||
description: internal server error
|
||||
schema:
|
||||
type: string
|
||||
summary: Get image manifest
|
||||
head:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Check an image's manifest given a reference or a digest
|
||||
parameters:
|
||||
- description: repository name
|
||||
in: path
|
||||
name: name
|
||||
required: true
|
||||
type: string
|
||||
- description: image reference or digest
|
||||
in: path
|
||||
name: reference
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: ok
|
||||
headers:
|
||||
api.DistContentDigestKey:
|
||||
description: OK
|
||||
type: object
|
||||
schema:
|
||||
type: string
|
||||
"404":
|
||||
description: not found
|
||||
schema:
|
||||
type: string
|
||||
"500":
|
||||
description: internal server error
|
||||
schema:
|
||||
type: string
|
||||
summary: Check image manifest
|
||||
put:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Update an image's manifest given a reference or a digest
|
||||
parameters:
|
||||
- description: repository name
|
||||
in: path
|
||||
name: name
|
||||
required: true
|
||||
type: string
|
||||
- description: image reference or digest
|
||||
in: path
|
||||
name: reference
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"201":
|
||||
description: created
|
||||
schema:
|
||||
type: string
|
||||
"400":
|
||||
description: bad request
|
||||
schema:
|
||||
type: string
|
||||
"404":
|
||||
description: not found
|
||||
schema:
|
||||
type: string
|
||||
"500":
|
||||
description: internal server error
|
||||
schema:
|
||||
type: string
|
||||
summary: Update image manifest
|
||||
/v2/{name}/tags/list:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: List all image tags in a repository
|
||||
parameters:
|
||||
- description: test
|
||||
in: path
|
||||
name: name
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/api.ImageTags'
|
||||
type: object
|
||||
"404":
|
||||
description: not found
|
||||
schema:
|
||||
type: string
|
||||
summary: List image tags
|
||||
swagger: "2.0"
|
8
errors/BUILD.bazel
Normal file
8
errors/BUILD.bazel
Normal file
|
@ -0,0 +1,8 @@
|
|||
load("@io_bazel_rules_go//go:def.bzl", "go_library")
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["errors.go"],
|
||||
importpath = "github.com/anuvu/zot/errors",
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
18
errors/errors.go
Normal file
18
errors/errors.go
Normal file
|
@ -0,0 +1,18 @@
|
|||
package errors
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrBadConfig = errors.New("config: invalid config")
|
||||
ErrRepoNotFound = errors.New("repository: not found")
|
||||
ErrRepoIsNotDir = errors.New("repository: not a directory")
|
||||
ErrRepoBadVersion = errors.New("repository: unsupported layout version")
|
||||
ErrManifestNotFound = errors.New("manifest: not found")
|
||||
ErrBadManifest = errors.New("manifest: invalid contents")
|
||||
ErrUploadNotFound = errors.New("uploads: not found")
|
||||
ErrBadUploadRange = errors.New("uploads: bad range")
|
||||
ErrBlobNotFound = errors.New("blob: not found")
|
||||
ErrBadBlob = errors.New("blob: bad blob")
|
||||
ErrBadBlobDigest = errors.New("blob: bad blob digest")
|
||||
ErrUnknownCode = errors.New("error: unknown error code")
|
||||
)
|
25
examples/config-example.json
Normal file
25
examples/config-example.json
Normal file
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"version":"0.1.0-dev",
|
||||
"storage":{
|
||||
"rootDirectory":"/tmp/zot"
|
||||
},
|
||||
"http": {
|
||||
"address":"127.0.0.1",
|
||||
"port":"8080",
|
||||
"realm":"zot",
|
||||
"tls": {
|
||||
"cert":"test/data/server.crt",
|
||||
"key":"test/data/server.key"
|
||||
},
|
||||
"auth": {
|
||||
"htpasswd": {
|
||||
"path": "test/data/htpasswd"
|
||||
},
|
||||
"failDelay": 5
|
||||
}
|
||||
},
|
||||
"log":{
|
||||
"level":"debug",
|
||||
"output":"/tmp/zot.log"
|
||||
}
|
||||
}
|
19
examples/config-example.yaml
Normal file
19
examples/config-example.yaml
Normal file
|
@ -0,0 +1,19 @@
|
|||
---
|
||||
version:
|
||||
0.1.0-dev
|
||||
storage:
|
||||
rootDirectory: /tmp/zot
|
||||
http:
|
||||
address: 127.0.0.1
|
||||
port: 8080
|
||||
realm: zot
|
||||
tls:
|
||||
cert: test/data/server.crt
|
||||
key: test/data/server.key
|
||||
auth:
|
||||
htpasswd:
|
||||
path: test/data/htpasswd
|
||||
failDelay: 5
|
||||
log:
|
||||
level: debug
|
||||
output: /tmp/zot.log
|
13
examples/config-test.json
Normal file
13
examples/config-test.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"version":"0.1.0-dev",
|
||||
"storage":{
|
||||
"rootDirectory":"/tmp/zot"
|
||||
},
|
||||
"http": {
|
||||
"address":"127.0.0.1",
|
||||
"port":"8080"
|
||||
},
|
||||
"log":{
|
||||
"level":"debug"
|
||||
}
|
||||
}
|
22
go.mod
Normal file
22
go.mod
Normal file
|
@ -0,0 +1,22 @@
|
|||
module github.com/anuvu/zot
|
||||
|
||||
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/mitchellh/mapstructure v1.1.2
|
||||
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/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
|
||||
gopkg.in/resty.v1 v1.12.0
|
||||
)
|
242
go.sum
Normal file
242
go.sum
Normal file
|
@ -0,0 +1,242 @@
|
|||
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/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=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
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=
|
||||
github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
|
||||
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
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/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=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
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=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/opencontainers/distribution-spec v1.0.0-rc0 h1:xMzwhweo1gjvEo74mQjGTLau0TD3ACyTEC1310NbuSQ=
|
||||
github.com/opencontainers/distribution-spec v1.0.0-rc0/go.mod h1:copR2flp+jTEvQIFMb6MIx45OkrxzqyjszPDT3hx/5Q=
|
||||
github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ=
|
||||
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
|
||||
github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
|
||||
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
|
||||
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
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=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||
github.com/rs/zerolog v1.14.3 h1:4EGfSkR2hJDB0s3oFfrlPqjU1e4WLncergLil3nEKW0=
|
||||
github.com/rs/zerolog v1.14.3/go.mod h1:3WXPzbXEEliJ+a6UFE4vhIxV8qR1EML6ngzP9ug4eYg=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
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/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=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s=
|
||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
||||
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU=
|
||||
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
|
||||
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/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=
|
||||
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
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/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=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
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=
|
||||
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606050223-4d9ae51c2468 h1:fTfk6GjmihJbK0mSUFgPPgYpsdmApQ86Mcd4GuKax9U=
|
||||
golang.org/x/tools v0.0.0-20190606050223-4d9ae51c2468/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
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=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
47
pkg/api/BUILD.bazel
Normal file
47
pkg/api/BUILD.bazel
Normal file
|
@ -0,0 +1,47 @@
|
|||
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"auth.go",
|
||||
"config.go",
|
||||
"controller.go",
|
||||
"errors.go",
|
||||
"log.go",
|
||||
"routes.go",
|
||||
],
|
||||
importpath = "github.com/anuvu/zot/pkg/api",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//docs:go_default_library",
|
||||
"//errors:go_default_library",
|
||||
"//pkg/storage:go_default_library",
|
||||
"@com_github_gin_gonic_gin//: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",
|
||||
"@org_golang_x_crypto//bcrypt:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
timeout = "short",
|
||||
srcs = [
|
||||
"controller_test.go",
|
||||
"routes_test.go",
|
||||
],
|
||||
data = [
|
||||
"//:exported_testdata",
|
||||
],
|
||||
embed = [":go_default_library"],
|
||||
race = "on",
|
||||
deps = [
|
||||
"@com_github_opencontainers_go_digest//:go_default_library",
|
||||
"@com_github_opencontainers_image_spec//specs-go/v1:go_default_library",
|
||||
"@com_github_smartystreets_goconvey//convey:go_default_library",
|
||||
"@in_gopkg_resty_v1//:go_default_library",
|
||||
],
|
||||
)
|
91
pkg/api/auth.go
Normal file
91
pkg/api/auth.go
Normal file
|
@ -0,0 +1,91 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func authFail(ginCtx *gin.Context, realm string, delay int) {
|
||||
time.Sleep(time.Duration(delay) * time.Second)
|
||||
ginCtx.Header("WWW-Authenticate", realm)
|
||||
ginCtx.AbortWithStatusJSON(http.StatusUnauthorized, NewError(UNAUTHORIZED))
|
||||
}
|
||||
|
||||
func BasicAuthHandler(c *Controller) gin.HandlerFunc {
|
||||
if c.Config.HTTP.Auth.HTPasswd.Path == "" {
|
||||
// no authentication
|
||||
return func(ginCtx *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
realm := c.Config.HTTP.Realm
|
||||
if realm == "" {
|
||||
realm = "Authorization Required"
|
||||
}
|
||||
realm = "Basic realm=" + strconv.Quote(realm)
|
||||
delay := c.Config.HTTP.Auth.FailDelay
|
||||
credMap := make(map[string]string)
|
||||
|
||||
f, err := os.Open(c.Config.HTTP.Auth.HTPasswd.Path)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for {
|
||||
r := bufio.NewReader(f)
|
||||
line, err := r.ReadString('\n')
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
tokens := strings.Split(line, ":")
|
||||
credMap[tokens[0]] = tokens[1]
|
||||
}
|
||||
|
||||
return func(ginCtx *gin.Context) {
|
||||
basicAuth := ginCtx.Request.Header.Get("Authorization")
|
||||
if basicAuth == "" {
|
||||
authFail(ginCtx, realm, delay)
|
||||
return
|
||||
}
|
||||
|
||||
s := strings.SplitN(basicAuth, " ", 2)
|
||||
if len(s) != 2 || strings.ToLower(s[0]) != "basic" {
|
||||
authFail(ginCtx, realm, delay)
|
||||
return
|
||||
}
|
||||
|
||||
b, err := base64.StdEncoding.DecodeString(s[1])
|
||||
if err != nil {
|
||||
authFail(ginCtx, realm, delay)
|
||||
return
|
||||
}
|
||||
|
||||
pair := strings.SplitN(string(b), ":", 2)
|
||||
if len(pair) != 2 {
|
||||
authFail(ginCtx, realm, delay)
|
||||
return
|
||||
}
|
||||
|
||||
username := pair[0]
|
||||
passphrase := pair[1]
|
||||
|
||||
passphraseHash, ok := credMap[username]
|
||||
if !ok {
|
||||
authFail(ginCtx, realm, delay)
|
||||
return
|
||||
}
|
||||
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(passphraseHash), []byte(passphrase)); err != nil {
|
||||
authFail(ginCtx, realm, delay)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
52
pkg/api/config.go
Normal file
52
pkg/api/config.go
Normal file
|
@ -0,0 +1,52 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
dspec "github.com/opencontainers/distribution-spec"
|
||||
)
|
||||
|
||||
type StorageConfig struct {
|
||||
RootDirectory string
|
||||
}
|
||||
|
||||
type TLSConfig struct {
|
||||
Cert string
|
||||
Key string
|
||||
CACert string
|
||||
}
|
||||
|
||||
type AuthHTPasswd struct {
|
||||
Path string
|
||||
}
|
||||
|
||||
type AuthConfig struct {
|
||||
FailDelay int
|
||||
HTPasswd AuthHTPasswd
|
||||
}
|
||||
|
||||
type HTTPConfig struct {
|
||||
Address string
|
||||
Port string
|
||||
TLS TLSConfig `mapstructure:",omitempty"`
|
||||
Auth AuthConfig `mapstructure:",omitempty"`
|
||||
Realm string
|
||||
}
|
||||
|
||||
type LogConfig struct {
|
||||
Level string
|
||||
Output string
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Version string
|
||||
Storage StorageConfig
|
||||
HTTP HTTPConfig
|
||||
Log LogConfig `mapstructure:",omitempty"`
|
||||
}
|
||||
|
||||
func NewConfig() *Config {
|
||||
return &Config{
|
||||
Version: dspec.Version,
|
||||
HTTP: HTTPConfig{Address: "127.0.0.1", Port: "8080"},
|
||||
Log: LogConfig{Level: "debug"},
|
||||
}
|
||||
}
|
69
pkg/api/controller.go
Normal file
69
pkg/api/controller.go
Normal file
|
@ -0,0 +1,69 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/anuvu/zot/pkg/storage"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type Controller struct {
|
||||
Config *Config
|
||||
Router *gin.Engine
|
||||
ImageStore *storage.ImageStore
|
||||
Log zerolog.Logger
|
||||
Server *http.Server
|
||||
}
|
||||
|
||||
func NewController(config *Config) *Controller {
|
||||
return &Controller{Config: config, Log: NewLogger(config)}
|
||||
}
|
||||
|
||||
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))
|
||||
c.Router = engine
|
||||
_ = NewRouteHandler(c)
|
||||
|
||||
c.Log.Info().Interface("params", c.Config).Msg("configuration settings")
|
||||
c.ImageStore = storage.NewImageStore(c.Config.Storage.RootDirectory, c.Log)
|
||||
|
||||
addr := fmt.Sprintf("%s:%s", c.Config.HTTP.Address, c.Config.HTTP.Port)
|
||||
server := &http.Server{Addr: addr, Handler: c.Router}
|
||||
c.Server = server
|
||||
|
||||
// Create the listener
|
||||
l, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.Config.HTTP.TLS.Key != "" && c.Config.HTTP.TLS.Cert != "" {
|
||||
if c.Config.HTTP.TLS.CACert != "" {
|
||||
caCert, err := ioutil.ReadFile(c.Config.HTTP.TLS.CACert)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
caCertPool := x509.NewCertPool()
|
||||
caCertPool.AppendCertsFromPEM(caCert)
|
||||
server.TLSConfig = &tls.Config{
|
||||
ClientAuth: tls.RequireAndVerifyClientCert,
|
||||
ClientCAs: caCertPool,
|
||||
}
|
||||
}
|
||||
|
||||
return server.ServeTLS(l, c.Config.HTTP.TLS.Cert, c.Config.HTTP.TLS.Key)
|
||||
}
|
||||
return server.Serve(l)
|
||||
}
|
237
pkg/api/controller_test.go
Normal file
237
pkg/api/controller_test.go
Normal file
|
@ -0,0 +1,237 @@
|
|||
package api_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/anuvu/zot/pkg/api"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
"gopkg.in/resty.v1"
|
||||
)
|
||||
|
||||
const (
|
||||
BaseURL1 = "http://127.0.0.1:8081"
|
||||
BaseURL2 = "http://127.0.0.1:8082"
|
||||
BaseSecureURL2 = "https://127.0.0.1:8082"
|
||||
username = "test"
|
||||
passphrase = "test"
|
||||
htpasswdPath = "../../test/data/htpasswd" // nolint (gosec) - this is just test data
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
Convey("Make a new controller", t, func() {
|
||||
config := api.NewConfig()
|
||||
So(config, ShouldNotBeNil)
|
||||
So(api.NewController(config), ShouldNotBeNil)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBasicAuth(t *testing.T) {
|
||||
Convey("Make a new controller", t, func() {
|
||||
config := api.NewConfig()
|
||||
config.HTTP.Port = "8081"
|
||||
config.HTTP.Auth.HTPasswd.Path = htpasswdPath
|
||||
c := api.NewController(config)
|
||||
dir, err := ioutil.TempDir("", "oci-repo-test")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
c.Config.Storage.RootDirectory = dir
|
||||
go func() {
|
||||
// this blocks
|
||||
if err := c.Run(); err != nil {
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
// wait till ready
|
||||
for {
|
||||
_, err := resty.R().Get(BaseURL1)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
ctx := context.Background()
|
||||
_ = c.Server.Shutdown(ctx)
|
||||
}()
|
||||
|
||||
// without creds, should get access error
|
||||
resp, err := resty.R().Get(BaseURL1)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 401)
|
||||
var e api.Error
|
||||
err = json.Unmarshal(resp.Body(), &e)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// with creds, should get expected status code
|
||||
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(BaseURL1)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 404)
|
||||
|
||||
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(BaseURL1 + "/v2/")
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 200)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTLSWithBasicAuth(t *testing.T) {
|
||||
Convey("Make a new controller", t, func() {
|
||||
caCert, err := ioutil.ReadFile("../../test/data/ca.crt")
|
||||
So(err, ShouldBeNil)
|
||||
caCertPool := x509.NewCertPool()
|
||||
caCertPool.AppendCertsFromPEM(caCert)
|
||||
|
||||
resty.SetTLSClientConfig(&tls.Config{RootCAs: caCertPool})
|
||||
defer func() { resty.SetTLSClientConfig(nil) }()
|
||||
config := api.NewConfig()
|
||||
config.HTTP.Port = "8082"
|
||||
config.HTTP.Auth.HTPasswd.Path = htpasswdPath
|
||||
config.HTTP.TLS.Cert = "../../test/data/server.crt"
|
||||
config.HTTP.TLS.Key = "../../test/data/server.key"
|
||||
|
||||
c := api.NewController(config)
|
||||
dir, err := ioutil.TempDir("", "oci-repo-test")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
c.Config.Storage.RootDirectory = dir
|
||||
go func() {
|
||||
// this blocks
|
||||
if err := c.Run(); err != nil {
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
// wait till ready
|
||||
for {
|
||||
_, err := resty.R().Get(BaseURL2)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
ctx := context.Background()
|
||||
_ = c.Server.Shutdown(ctx)
|
||||
}()
|
||||
|
||||
// accessing insecure HTTP site should fail
|
||||
resp, err := resty.R().Get(BaseURL2)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 400)
|
||||
|
||||
// without creds, should get access error
|
||||
resp, err = resty.R().Get(BaseSecureURL2)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 401)
|
||||
var e api.Error
|
||||
err = json.Unmarshal(resp.Body(), &e)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// with creds, should get expected status code
|
||||
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(BaseSecureURL2)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 404)
|
||||
|
||||
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(BaseSecureURL2 + "/v2/")
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 200)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTLSMutualAuth(t *testing.T) {
|
||||
Convey("Make a new controller", t, func() {
|
||||
caCert, err := ioutil.ReadFile("../../test/data/ca.crt")
|
||||
So(err, ShouldBeNil)
|
||||
caCertPool := x509.NewCertPool()
|
||||
caCertPool.AppendCertsFromPEM(caCert)
|
||||
|
||||
resty.SetTLSClientConfig(&tls.Config{RootCAs: caCertPool})
|
||||
defer func() { resty.SetTLSClientConfig(nil) }()
|
||||
config := api.NewConfig()
|
||||
config.HTTP.Port = "8082"
|
||||
config.HTTP.Auth.HTPasswd.Path = htpasswdPath
|
||||
config.HTTP.TLS.Cert = "../../test/data/server.crt"
|
||||
config.HTTP.TLS.Key = "../../test/data/server.key"
|
||||
config.HTTP.TLS.CACert = "../../test/data/ca.crt"
|
||||
|
||||
c := api.NewController(config)
|
||||
dir, err := ioutil.TempDir("", "oci-repo-test")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
c.Config.Storage.RootDirectory = dir
|
||||
go func() {
|
||||
// this blocks
|
||||
if err := c.Run(); err != nil {
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
// wait till ready
|
||||
for {
|
||||
_, err := resty.R().Get(BaseURL2)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
ctx := context.Background()
|
||||
_ = c.Server.Shutdown(ctx)
|
||||
}()
|
||||
|
||||
// accessing insecure HTTP site should fail
|
||||
resp, err := resty.R().Get(BaseURL2)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 400)
|
||||
|
||||
// without client certs and creds, should get conn error
|
||||
_, err = resty.R().Get(BaseSecureURL2)
|
||||
So(err, ShouldNotBeNil)
|
||||
|
||||
// with creds but without certs, should get conn error
|
||||
_, err = resty.R().SetBasicAuth(username, passphrase).Get(BaseSecureURL2)
|
||||
So(err, ShouldNotBeNil)
|
||||
|
||||
// setup TLS mutual auth
|
||||
cert, err := tls.LoadX509KeyPair("../../test/data/client.crt", "../../test/data/client.key")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
resty.SetCertificates(cert)
|
||||
defer func() { resty.SetCertificates(tls.Certificate{}) }()
|
||||
|
||||
// with client certs but without creds, should get access error
|
||||
resp, err = resty.R().Get(BaseSecureURL2)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 401)
|
||||
|
||||
// with client certs and creds, should get expected status code
|
||||
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(BaseSecureURL2)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 404)
|
||||
|
||||
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(BaseSecureURL2 + "/v2/")
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 200)
|
||||
})
|
||||
}
|
142
pkg/api/errors.go
Normal file
142
pkg/api/errors.go
Normal file
|
@ -0,0 +1,142 @@
|
|||
package api
|
||||
|
||||
import "github.com/anuvu/zot/errors"
|
||||
|
||||
type Error struct {
|
||||
Code ErrorCode `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Description string `json:"description"`
|
||||
Detail interface{} `json:"detail,omitempty"`
|
||||
}
|
||||
|
||||
type ErrorList struct {
|
||||
Errors []*Error `json:"errors"`
|
||||
}
|
||||
|
||||
type ErrorCode int
|
||||
|
||||
// nolint (golint)
|
||||
const (
|
||||
BLOB_UNKNOWN ErrorCode = iota
|
||||
BLOB_UPLOAD_INVALID
|
||||
BLOB_UPLOAD_UNKNOWN
|
||||
DIGEST_INVALID
|
||||
MANIFEST_BLOB_UNKNOWN
|
||||
MANIFEST_INVALID
|
||||
MANIFEST_UNKNOWN
|
||||
MANIFEST_UNVERIFIED
|
||||
NAME_INVALID
|
||||
NAME_UNKNOWN
|
||||
SIZE_INVALID
|
||||
TAG_INVALID
|
||||
UNAUTHORIZED
|
||||
DENIED
|
||||
UNSUPPORTED
|
||||
)
|
||||
|
||||
func NewError(code ErrorCode, detail ...interface{}) Error {
|
||||
|
||||
var errMap = map[ErrorCode]Error{
|
||||
BLOB_UNKNOWN: {
|
||||
Message: "blob unknown to registry",
|
||||
Description: "blob unknown to registry This error MAY be returned when a blob is unknown " +
|
||||
" to the registry in a specified repository. This can be returned with a standard get or " +
|
||||
"if a manifest references an unknown layer during upload.",
|
||||
},
|
||||
|
||||
BLOB_UPLOAD_INVALID: {
|
||||
Message: "blob upload invalid",
|
||||
Description: `The blob upload encountered an error and can no longer proceed.`,
|
||||
},
|
||||
|
||||
BLOB_UPLOAD_UNKNOWN: {
|
||||
Message: "blob upload unknown to registry",
|
||||
Description: `If a blob upload has been cancelled or was never started, this error code MAY be returned.`,
|
||||
},
|
||||
|
||||
DIGEST_INVALID: {
|
||||
Message: "provided digest did not match uploaded content",
|
||||
Description: "When a blob is uploaded, the registry will check that the content matches the " +
|
||||
"digest provided by the client. The error MAY include a detail structure with the key " +
|
||||
"\"digest\", including the invalid digest string. This error MAY also be returned when " +
|
||||
"a manifest includes an invalid layer digest.",
|
||||
},
|
||||
|
||||
MANIFEST_BLOB_UNKNOWN: {
|
||||
Message: "blob unknown to registry",
|
||||
Description: `This error MAY be returned when a manifest blob is unknown
|
||||
to the registry.`,
|
||||
},
|
||||
|
||||
MANIFEST_INVALID: {
|
||||
Message: "manifest invalid",
|
||||
Description: `During upload, manifests undergo several checks ensuring
|
||||
validity. If those checks fail, this error MAY be returned, unless a more
|
||||
specific error is included. The detail will contain information the failed
|
||||
validation.`,
|
||||
},
|
||||
|
||||
MANIFEST_UNKNOWN: {
|
||||
Message: "manifest unknown",
|
||||
Description: `This error is returned when the manifest, identified by name
|
||||
and tag is unknown to the repository.`,
|
||||
},
|
||||
|
||||
MANIFEST_UNVERIFIED: {
|
||||
Message: "manifest failed signature verification",
|
||||
Description: `During manifest upload, if the manifest fails signature
|
||||
verification, this error will be returned.`,
|
||||
},
|
||||
|
||||
NAME_INVALID: {
|
||||
Message: "invalid repository name",
|
||||
Description: `Invalid repository name encountered either during manifest
|
||||
validation or any API operation.`,
|
||||
},
|
||||
|
||||
NAME_UNKNOWN: {
|
||||
Message: "repository name not known to registry",
|
||||
Description: `This is returned if the name used during an operation is unknown to the registry.`,
|
||||
},
|
||||
|
||||
SIZE_INVALID: {
|
||||
Message: "provided length did not match content length",
|
||||
Description: "When a layer is uploaded, the provided size will be checked against the uploaded " +
|
||||
"content. If they do not match, this error will be returned.",
|
||||
},
|
||||
|
||||
TAG_INVALID: {
|
||||
Message: "manifest tag did not match URI",
|
||||
Description: `During a manifest upload, if the tag in the manifest does
|
||||
not match the uri tag, this error will be returned.`,
|
||||
},
|
||||
|
||||
UNAUTHORIZED: {
|
||||
Message: "authentication required",
|
||||
Description: `The access controller was unable to authenticate the client.
|
||||
Often this will be accompanied by a Www-Authenticate HTTP response header
|
||||
indicating how to authenticate.`,
|
||||
},
|
||||
|
||||
DENIED: {
|
||||
Message: "requested access to the resource is denied",
|
||||
Description: `The access controller denied access for the operation on a
|
||||
resource.`,
|
||||
},
|
||||
|
||||
UNSUPPORTED: {
|
||||
Message: "The operation is unsupported.",
|
||||
Description: `The operation was unsupported due to a missing
|
||||
implementation or invalid set of parameters.`,
|
||||
},
|
||||
}
|
||||
|
||||
e, ok := errMap[code]
|
||||
if !ok {
|
||||
panic(errors.ErrUnknownCode)
|
||||
}
|
||||
|
||||
e.Code = code
|
||||
e.Detail = detail
|
||||
return e
|
||||
}
|
70
pkg/api/log.go
Normal file
70
pkg/api/log.go
Normal file
|
@ -0,0 +1,70 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func NewLogger(config *Config) zerolog.Logger {
|
||||
zerolog.TimeFieldFormat = time.RFC3339Nano
|
||||
lvl, err := zerolog.ParseLevel(config.Log.Level)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
zerolog.SetGlobalLevel(lvl)
|
||||
var log zerolog.Logger
|
||||
if config.Log.Output == "" {
|
||||
log = zerolog.New(os.Stdout)
|
||||
} else {
|
||||
file, err := os.OpenFile(config.Log.Output, os.O_WRONLY|os.O_CREATE, 0600)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
log = zerolog.New(file)
|
||||
}
|
||||
return log.With().Timestamp().Logger()
|
||||
}
|
||||
|
||||
func Logger(log zerolog.Logger) gin.HandlerFunc {
|
||||
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
|
||||
|
||||
// Process request
|
||||
ginCtx.Next()
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
821
pkg/api/routes.go
Normal file
821
pkg/api/routes.go
Normal file
|
@ -0,0 +1,821 @@
|
|||
// @title Open Container Initiative Distribution Specification
|
||||
// @version v0.1.0-dev
|
||||
// @description APIs for Open Container Initiative Distribution Specification
|
||||
|
||||
// @contact.name API Support
|
||||
// @contact.url http://www.swagger.io/support
|
||||
// @contact.email support@swagger.io
|
||||
|
||||
// @license.name Apache 2.0
|
||||
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
_ "github.com/anuvu/zot/docs" // nolint (golint) - as required by swaggo
|
||||
"github.com/anuvu/zot/errors"
|
||||
"github.com/gin-gonic/gin"
|
||||
ispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
ginSwagger "github.com/swaggo/gin-swagger"
|
||||
"github.com/swaggo/gin-swagger/swaggerFiles"
|
||||
)
|
||||
|
||||
const RoutePrefix = "/v2"
|
||||
const DistContentDigestKey = "Docker-Content-Digest"
|
||||
const BlobUploadUUID = "Blob-Upload-UUID"
|
||||
|
||||
type RouteHandler struct {
|
||||
c *Controller
|
||||
}
|
||||
|
||||
func NewRouteHandler(c *Controller) *RouteHandler {
|
||||
rh := &RouteHandler{c: c}
|
||||
rh.SetupRoutes()
|
||||
return rh
|
||||
}
|
||||
|
||||
func (rh *RouteHandler) SetupRoutes() {
|
||||
rh.c.Router.Use(BasicAuthHandler(rh.c))
|
||||
g := rh.c.Router.Group(RoutePrefix)
|
||||
{
|
||||
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)
|
||||
}
|
||||
// swagger docs "/swagger/v2/index.html"
|
||||
rh.c.Router.GET("/swagger/v2/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
||||
}
|
||||
|
||||
// Method handlers
|
||||
|
||||
// CheckVersionSupport godoc
|
||||
// @Summary Check API support
|
||||
// @Description Check if this API version is supported
|
||||
// @Router /v2/ [get]
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {string} string "ok"
|
||||
func (rh *RouteHandler) CheckVersionSupport(ginCtx *gin.Context) {
|
||||
ginCtx.Data(http.StatusOK, "application/json; charset=utf-8", []byte{})
|
||||
}
|
||||
|
||||
type ImageTags struct {
|
||||
Name string `json:"name"`
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
|
||||
// ListTags godoc
|
||||
// @Summary List image tags
|
||||
// @Description List all image tags in a repository
|
||||
// @Router /v2/{name}/tags/list [get]
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @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)
|
||||
return
|
||||
}
|
||||
|
||||
tags, err := rh.c.ImageStore.GetImageTags(name)
|
||||
if err != nil {
|
||||
ginCtx.JSON(http.StatusNotFound, NewError(NAME_UNKNOWN, map[string]string{"name": name}))
|
||||
return
|
||||
}
|
||||
|
||||
ginCtx.JSON(http.StatusOK, ImageTags{Name: name, Tags: tags})
|
||||
}
|
||||
|
||||
// CheckManifest godoc
|
||||
// @Summary Check image manifest
|
||||
// @Description Check an image's manifest given a reference or a digest
|
||||
// @Router /v2/{name}/manifests/{reference} [head]
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param name path string true "repository name"
|
||||
// @Param reference path string true "image reference or digest"
|
||||
// @Success 200 {string} string "ok"
|
||||
// @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)
|
||||
return
|
||||
}
|
||||
|
||||
reference := ginCtx.Param("reference")
|
||||
if reference == "" {
|
||||
ginCtx.JSON(http.StatusNotFound, NewError(MANIFEST_INVALID, map[string]string{"reference": reference}))
|
||||
return
|
||||
}
|
||||
|
||||
_, digest, _, err := rh.c.ImageStore.GetImageManifest(name, reference)
|
||||
if err != nil {
|
||||
switch err {
|
||||
case errors.ErrManifestNotFound:
|
||||
ginCtx.JSON(http.StatusNotFound, NewError(MANIFEST_UNKNOWN, map[string]string{"reference": reference}))
|
||||
default:
|
||||
ginCtx.JSON(http.StatusInternalServerError, NewError(MANIFEST_INVALID, map[string]string{"reference": reference}))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ginCtx.Status(http.StatusOK)
|
||||
ginCtx.Header(DistContentDigestKey, digest)
|
||||
ginCtx.Header("Content-Length", "0")
|
||||
}
|
||||
|
||||
// NOTE: https://github.com/swaggo/swag/issues/387
|
||||
type ImageManifest struct {
|
||||
ispec.Manifest
|
||||
}
|
||||
|
||||
// GetManifest godoc
|
||||
// @Summary Get image manifest
|
||||
// @Description Get an image's manifest given a reference or a digest
|
||||
// @Accept json
|
||||
// @Produce application/vnd.oci.image.manifest.v1+json
|
||||
// @Param name path string true "repository name"
|
||||
// @Param reference path string true "image reference or digest"
|
||||
// @Success 200 {object} api.ImageManifest
|
||||
// @Header 200 {object} api.DistContentDigestKey
|
||||
// @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)
|
||||
return
|
||||
}
|
||||
|
||||
reference := ginCtx.Param("reference")
|
||||
if reference == "" {
|
||||
ginCtx.JSON(http.StatusNotFound, NewError(MANIFEST_UNKNOWN, map[string]string{"reference": reference}))
|
||||
return
|
||||
}
|
||||
|
||||
content, digest, mediaType, err := rh.c.ImageStore.GetImageManifest(name, reference)
|
||||
if err != nil {
|
||||
switch err {
|
||||
case errors.ErrRepoNotFound:
|
||||
case errors.ErrRepoBadVersion:
|
||||
ginCtx.JSON(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}))
|
||||
default:
|
||||
ginCtx.Status(http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ginCtx.Data(http.StatusOK, mediaType, content)
|
||||
ginCtx.Header(DistContentDigestKey, digest)
|
||||
}
|
||||
|
||||
// UpdateManifest godoc
|
||||
// @Summary Update image manifest
|
||||
// @Description Update an image's manifest given a reference or a digest
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param name path string true "repository name"
|
||||
// @Param reference path string true "image reference or digest"
|
||||
// @Header 201 {object} api.DistContentDigestKey
|
||||
// @Success 201 {string} string "created"
|
||||
// @Failure 400 {string} string "bad request"
|
||||
// @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)
|
||||
return
|
||||
}
|
||||
|
||||
reference := ginCtx.Param("reference")
|
||||
if reference == "" {
|
||||
ginCtx.JSON(http.StatusNotFound, NewError(MANIFEST_INVALID, map[string]string{"reference": reference}))
|
||||
return
|
||||
}
|
||||
|
||||
mediaType := ginCtx.ContentType()
|
||||
if mediaType != ispec.MediaTypeImageManifest {
|
||||
ginCtx.Status(http.StatusUnsupportedMediaType)
|
||||
return
|
||||
}
|
||||
|
||||
body, err := ginCtx.GetRawData()
|
||||
if err != nil {
|
||||
ginCtx.Status(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
digest, err := rh.c.ImageStore.PutImageManifest(name, reference, mediaType, body)
|
||||
if err != nil {
|
||||
switch err {
|
||||
case errors.ErrRepoNotFound:
|
||||
ginCtx.JSON(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}))
|
||||
case errors.ErrBadManifest:
|
||||
ginCtx.JSON(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}))
|
||||
default:
|
||||
ginCtx.Status(http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ginCtx.Status(http.StatusCreated)
|
||||
ginCtx.Header("Location", fmt.Sprintf("/v2/%s/manifests/%s", name, digest))
|
||||
ginCtx.Header(DistContentDigestKey, digest)
|
||||
}
|
||||
|
||||
// DeleteManifest godoc
|
||||
// @Summary Delete image manifest
|
||||
// @Description Delete an image's manifest given a reference or a digest
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param name path string true "repository name"
|
||||
// @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)
|
||||
return
|
||||
}
|
||||
|
||||
reference := ginCtx.Param("reference")
|
||||
if reference == "" {
|
||||
ginCtx.Status(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
err := rh.c.ImageStore.DeleteImageManifest(name, reference)
|
||||
if err != nil {
|
||||
switch err {
|
||||
case errors.ErrRepoNotFound:
|
||||
ginCtx.JSON(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}))
|
||||
default:
|
||||
ginCtx.Status(http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ginCtx.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
// CheckBlob godoc
|
||||
// @Summary Check image blob/layer
|
||||
// @Description Check an image's blob/layer given a digest
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param name path string true "repository name"
|
||||
// @Param digest path string true "blob/layer digest"
|
||||
// @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)
|
||||
return
|
||||
}
|
||||
|
||||
digest := ginCtx.Param("digest")
|
||||
if digest == "" {
|
||||
ginCtx.Status(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
mediaType := ginCtx.Request.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}))
|
||||
case errors.ErrRepoNotFound:
|
||||
ginCtx.JSON(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}))
|
||||
default:
|
||||
ginCtx.Status(http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if !ok {
|
||||
ginCtx.JSON(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)
|
||||
}
|
||||
|
||||
// GetBlob godoc
|
||||
// @Summary Get image blob/layer
|
||||
// @Description Get an image's blob/layer given a digest
|
||||
// @Accept json
|
||||
// @Produce application/vnd.oci.image.layer.v1.tar+gzip
|
||||
// @Param name path string true "repository name"
|
||||
// @Param digest path string true "blob/layer digest"
|
||||
// @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)
|
||||
return
|
||||
}
|
||||
|
||||
digest := ginCtx.Param("digest")
|
||||
if digest == "" {
|
||||
ginCtx.Status(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
mediaType := ginCtx.Request.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}))
|
||||
case errors.ErrRepoNotFound:
|
||||
ginCtx.JSON(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}))
|
||||
default:
|
||||
ginCtx.Status(http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ginCtx.Status(http.StatusOK)
|
||||
ginCtx.Header("Content-Length", fmt.Sprintf("%d", blen))
|
||||
ginCtx.Header(DistContentDigestKey, digest)
|
||||
// return the blob data
|
||||
ginCtx.DataFromReader(http.StatusOK, blen, mediaType, br, map[string]string{})
|
||||
}
|
||||
|
||||
// DeleteBlob godoc
|
||||
// @Summary Delete image blob/layer
|
||||
// @Description Delete an image's blob/layer given a digest
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param name path string true "repository name"
|
||||
// @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)
|
||||
return
|
||||
}
|
||||
|
||||
digest := ginCtx.Param("digest")
|
||||
if digest == "" {
|
||||
ginCtx.Status(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
err := rh.c.ImageStore.DeleteBlob(name, digest)
|
||||
if err != nil {
|
||||
switch err {
|
||||
case errors.ErrBadBlobDigest:
|
||||
ginCtx.JSON(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}))
|
||||
case errors.ErrBlobNotFound:
|
||||
ginCtx.JSON(http.StatusNotFound, NewError(BLOB_UNKNOWN, map[string]string{"digest": digest}))
|
||||
default:
|
||||
ginCtx.Status(http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ginCtx.Status(http.StatusAccepted)
|
||||
}
|
||||
|
||||
// CreateBlobUpload godoc
|
||||
// @Summary Create image blob/layer upload
|
||||
// @Description Create a new image blob/layer upload
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param name path string true "repository name"
|
||||
// @Success 202 {string} string "accepted"
|
||||
// @Header 202 {string} Location "/v2/{name}/blobs/uploads/{uuid}"
|
||||
// @Header 202 {string} Range "bytes=0-0"
|
||||
// @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)
|
||||
return
|
||||
}
|
||||
|
||||
u, err := rh.c.ImageStore.NewBlobUpload(name)
|
||||
if err != nil {
|
||||
switch err {
|
||||
case errors.ErrRepoNotFound:
|
||||
ginCtx.JSON(http.StatusNotFound, NewError(NAME_UNKNOWN, map[string]string{"name": name}))
|
||||
default:
|
||||
ginCtx.Status(http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ginCtx.Status(http.StatusAccepted)
|
||||
ginCtx.Header("Location", path.Join(ginCtx.Request.URL.String(), u))
|
||||
ginCtx.Header("Range", "bytes=0-0")
|
||||
}
|
||||
|
||||
// GetBlobUpload godoc
|
||||
// @Summary Get image blob/layer upload
|
||||
// @Description Get an image's blob/layer upload given a uuid
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param name path string true "repository name"
|
||||
// @Param uuid path string true "upload uuid"
|
||||
// @Success 204 {string} string "no content"
|
||||
// @Header 202 {string} Location "/v2/{name}/blobs/uploads/{uuid}"
|
||||
// @Header 202 {string} Range "bytes=0-128"
|
||||
// @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)
|
||||
return
|
||||
}
|
||||
|
||||
name := ginCtx.Param("name")
|
||||
if name == "" {
|
||||
ginCtx.Status(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
uuid := ginCtx.Param("uuid")
|
||||
if uuid == "" {
|
||||
ginCtx.Status(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
size, err := rh.c.ImageStore.GetBlobUpload(name, uuid)
|
||||
if err != nil {
|
||||
switch err {
|
||||
case errors.ErrBadUploadRange:
|
||||
case errors.ErrBadBlobDigest:
|
||||
ginCtx.JSON(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}))
|
||||
case errors.ErrUploadNotFound:
|
||||
ginCtx.JSON(http.StatusNotFound, NewError(BLOB_UPLOAD_UNKNOWN, map[string]string{"uuid": uuid}))
|
||||
default:
|
||||
ginCtx.Status(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))
|
||||
}
|
||||
|
||||
// PatchBlobUpload godoc
|
||||
// @Summary Resume image blob/layer upload
|
||||
// @Description Resume an image's blob/layer upload given an uuid
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param name path string true "repository name"
|
||||
// @Param uuid path string true "upload uuid"
|
||||
// @Success 202 {string} string "accepted"
|
||||
// @Header 202 {string} Location "/v2/{name}/blobs/uploads/{uuid}"
|
||||
// @Header 202 {string} Range "bytes=0-128"
|
||||
// @Header 200 {object} api.BlobUploadUUID
|
||||
// @Failure 400 {string} string "bad request"
|
||||
// @Failure 404 {string} string "not found"
|
||||
// @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)
|
||||
return
|
||||
}
|
||||
|
||||
name := ginCtx.Param("name")
|
||||
if name == "" {
|
||||
ginCtx.Status(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
uuid := ginCtx.Param("uuid")
|
||||
if uuid == "" {
|
||||
ginCtx.Status(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)
|
||||
return
|
||||
}
|
||||
|
||||
contentRange := ginCtx.Request.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)
|
||||
return
|
||||
}
|
||||
|
||||
var from, to int64
|
||||
if from, to, err = getContentRange(ginCtx); err != nil || (to-from) != contentLength {
|
||||
ginCtx.Status(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)
|
||||
return
|
||||
}
|
||||
|
||||
clen, err := rh.c.ImageStore.PutBlobChunk(name, uuid, from, to, ginCtx.Request.Body)
|
||||
if err != nil {
|
||||
switch err {
|
||||
case errors.ErrBadUploadRange:
|
||||
ginCtx.JSON(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}))
|
||||
case errors.ErrUploadNotFound:
|
||||
ginCtx.JSON(http.StatusNotFound, NewError(BLOB_UPLOAD_UNKNOWN, map[string]string{"uuid": uuid}))
|
||||
default:
|
||||
ginCtx.Status(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)
|
||||
}
|
||||
|
||||
// UpdateBlobUpload godoc
|
||||
// @Summary Update image blob/layer upload
|
||||
// @Description Update and finish an image's blob/layer upload given a digest
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param name path string true "repository name"
|
||||
// @Param uuid path string true "upload uuid"
|
||||
// @Param digest query string true "blob/layer digest"
|
||||
// @Success 201 {string} string "created"
|
||||
// @Header 202 {string} Location "/v2/{name}/blobs/{digest}"
|
||||
// @Header 200 {object} api.DistContentDigestKey
|
||||
// @Failure 404 {string} string "not found"
|
||||
// @Failure 500 {string} string "internal server error"
|
||||
// @Router /v2/{name}/blobs/uploads/{uuid} [put]
|
||||
func (rh *RouteHandler) UpdateBlobUpload(ginCtx *gin.Context) {
|
||||
if paramIsNot(ginCtx, "digest", "uploads") {
|
||||
ginCtx.Status(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
name := ginCtx.Param("name")
|
||||
if name == "" {
|
||||
ginCtx.Status(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
uuid := ginCtx.Param("uuid")
|
||||
if uuid == "" {
|
||||
ginCtx.Status(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
digest := ginCtx.Query("digest")
|
||||
if digest == "" {
|
||||
ginCtx.Status(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
contentPresent := true
|
||||
contentLen, err := strconv.ParseInt(ginCtx.Request.Header.Get("Content-Length"), 10, 64)
|
||||
if err != nil || contentLen == 0 {
|
||||
contentPresent = false
|
||||
}
|
||||
contentRangePresent := true
|
||||
if ginCtx.Request.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)
|
||||
return
|
||||
}
|
||||
|
||||
var from, to int64
|
||||
|
||||
if contentPresent {
|
||||
if ginCtx.ContentType() != "application/octet-stream" {
|
||||
ginCtx.Status(http.StatusUnsupportedMediaType)
|
||||
return
|
||||
}
|
||||
|
||||
contentRange := ginCtx.Request.Header.Get("Content-Range")
|
||||
if contentRange == "" { // monolithic upload
|
||||
from = 0
|
||||
if contentLen == 0 {
|
||||
ginCtx.Status(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
to = contentLen
|
||||
} else if from, to, err = getContentRange(ginCtx); err != nil { // finish chunked upload
|
||||
ginCtx.Status(http.StatusRequestedRangeNotSatisfiable)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = rh.c.ImageStore.PutBlobChunk(name, uuid, from, to, ginCtx.Request.Body)
|
||||
if err != nil {
|
||||
switch err {
|
||||
case errors.ErrBadUploadRange:
|
||||
ginCtx.JSON(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}))
|
||||
case errors.ErrUploadNotFound:
|
||||
ginCtx.JSON(http.StatusNotFound, NewError(BLOB_UPLOAD_UNKNOWN, map[string]string{"uuid": uuid}))
|
||||
default:
|
||||
ginCtx.Status(http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// blob chunks already transferred, just finish
|
||||
if err := rh.c.ImageStore.FinishBlobUpload(name, uuid, ginCtx.Request.Body, digest); err != nil {
|
||||
switch err {
|
||||
case errors.ErrBadBlobDigest:
|
||||
ginCtx.JSON(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}))
|
||||
case errors.ErrRepoNotFound:
|
||||
ginCtx.JSON(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}))
|
||||
default:
|
||||
ginCtx.Status(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)
|
||||
}
|
||||
|
||||
// DeleteBlobUpload godoc
|
||||
// @Summary Delete image blob/layer
|
||||
// @Description Delete an image's blob/layer given a digest
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param name path string true "repository name"
|
||||
// @Param uuid path string true "upload uuid"
|
||||
// @Success 200 {string} string "ok"
|
||||
// @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)
|
||||
return
|
||||
}
|
||||
|
||||
name := ginCtx.Param("name")
|
||||
uuid := ginCtx.Param("uuid")
|
||||
|
||||
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}))
|
||||
case errors.ErrUploadNotFound:
|
||||
ginCtx.JSON(http.StatusNotFound, NewError(BLOB_UPLOAD_UNKNOWN, map[string]string{"uuid": uuid}))
|
||||
default:
|
||||
ginCtx.Status(http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ginCtx.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
type RepositoryList struct {
|
||||
Repositories []string `json:"repositories"`
|
||||
}
|
||||
|
||||
// ListRepositories godoc
|
||||
// @Summary List image repositories
|
||||
// @Description List all image repositories
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @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
|
||||
}
|
||||
|
||||
repos, err := rh.c.ImageStore.GetRepositories()
|
||||
if err != nil {
|
||||
ginCtx.Status(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
is := RepositoryList{Repositories: repos}
|
||||
|
||||
ginCtx.JSON(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")
|
||||
tokens := strings.Split(contentRange, "-")
|
||||
from, err := strconv.ParseInt(tokens[0], 10, 64)
|
||||
if err != nil {
|
||||
return -1, -1, errors.ErrBadUploadRange
|
||||
}
|
||||
to, err := strconv.ParseInt(tokens[1], 10, 64)
|
||||
if err != nil {
|
||||
return -1, -1, errors.ErrBadUploadRange
|
||||
}
|
||||
if from > to {
|
||||
return -1, -1, errors.ErrBadUploadRange
|
||||
}
|
||||
return from, to, nil
|
||||
}
|
333
pkg/api/routes_test.go
Normal file
333
pkg/api/routes_test.go
Normal file
|
@ -0,0 +1,333 @@
|
|||
package api_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/anuvu/zot/pkg/api"
|
||||
godigest "github.com/opencontainers/go-digest"
|
||||
ispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
"gopkg.in/resty.v1"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultContentType = "application/json; charset=utf-8"
|
||||
BaseURL = "http://127.0.0.1:8080"
|
||||
)
|
||||
|
||||
func TestAPI(t *testing.T) {
|
||||
Convey("Make API calls to the controller", t, func(c C) {
|
||||
Convey("check version", func() {
|
||||
resp, err := resty.R().Get(BaseURL + "/v2/")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 200)
|
||||
})
|
||||
|
||||
Convey("Get repository catalog", func() {
|
||||
resp, err := resty.R().Get(BaseURL + "/v2/_catalog")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 200)
|
||||
So(resp.String(), ShouldNotBeEmpty)
|
||||
So(resp.Header().Get("Content-Type"), ShouldEqual, DefaultContentType)
|
||||
var repoList api.RepositoryList
|
||||
err = json.Unmarshal(resp.Body(), &repoList)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(repoList.Repositories), ShouldEqual, 0)
|
||||
})
|
||||
|
||||
Convey("Get images in a repository", func() {
|
||||
// non-existent repository should fail
|
||||
resp, err := resty.R().Get(BaseURL + "/v2/repo/tags/list")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 404)
|
||||
So(resp.String(), ShouldNotBeEmpty)
|
||||
|
||||
// after newly created upload should fail
|
||||
resp, err = resty.R().Post(BaseURL + "/v2/repo/blobs/uploads/")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 202)
|
||||
|
||||
resp, err = resty.R().Get(BaseURL + "/v2/repo/tags/list")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 200)
|
||||
So(resp.String(), ShouldNotBeEmpty)
|
||||
})
|
||||
|
||||
Convey("Monolithic blob upload", func() {
|
||||
resp, err := resty.R().Post(BaseURL + "/v2/repo/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/repo/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)
|
||||
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/")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 202)
|
||||
loc := resp.Header().Get("Location")
|
||||
So(loc, ShouldNotBeEmpty)
|
||||
|
||||
// delete this upload
|
||||
resp, err = resty.R().Delete(BaseURL + loc)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 200)
|
||||
})
|
||||
|
||||
Convey("Create and delete blobs", func() {
|
||||
// create a upload
|
||||
resp, err := resty.R().Post(BaseURL + "/v2/repo/blobs/uploads/")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 202)
|
||||
loc := resp.Header().Get("Location")
|
||||
So(loc, ShouldNotBeEmpty)
|
||||
|
||||
content := []byte("this is a blob")
|
||||
digest := godigest.FromBytes(content)
|
||||
So(digest, ShouldNotBeNil)
|
||||
// monolithic blob upload
|
||||
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(api.DistContentDigestKey), ShouldNotBeEmpty)
|
||||
|
||||
// delete this blob
|
||||
resp, err = resty.R().Delete(BaseURL + blobLoc)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 202)
|
||||
So(resp.Header().Get("Content-Length"), ShouldEqual, "0")
|
||||
})
|
||||
|
||||
Convey("Manifests", func() {
|
||||
// create a blob/layer
|
||||
resp, err := resty.R().Post(BaseURL + "/v2/repo/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)
|
||||
content := []byte("this is a blob")
|
||||
digest := godigest.FromBytes(content)
|
||||
So(digest, ShouldNotBeNil)
|
||||
// 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)
|
||||
|
||||
// create a manifest
|
||||
m := ispec.Manifest{Layers: []ispec.Descriptor{{Digest: digest}}}
|
||||
content, err = json.Marshal(m)
|
||||
So(err, ShouldBeNil)
|
||||
digest = godigest.FromBytes(content)
|
||||
So(digest, ShouldNotBeNil)
|
||||
resp, err = resty.R().SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json").
|
||||
SetBody(content).Put(BaseURL + "/v2/repo/manifests/test:1.0")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 201)
|
||||
d := resp.Header().Get(api.DistContentDigestKey)
|
||||
So(d, ShouldNotBeEmpty)
|
||||
So(d, ShouldEqual, digest.String())
|
||||
|
||||
// check/get by tag
|
||||
resp, err = resty.R().Head(BaseURL + "/v2/repo/manifests/test:1.0")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 200)
|
||||
resp, err = resty.R().Get(BaseURL + "/v2/repo/manifests/test:1.0")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 200)
|
||||
So(resp.Body(), ShouldNotBeEmpty)
|
||||
// check/get by reference
|
||||
resp, err = resty.R().Head(BaseURL + "/v2/repo/manifests/" + digest.String())
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 200)
|
||||
resp, err = resty.R().Get(BaseURL + "/v2/repo/manifests/" + digest.String())
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 200)
|
||||
So(resp.Body(), ShouldNotBeEmpty)
|
||||
|
||||
// delete manifest
|
||||
resp, err = resty.R().Delete(BaseURL + "/v2/repo/manifests/test:1.0")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 200)
|
||||
// delete again should fail
|
||||
resp, err = resty.R().Delete(BaseURL + "/v2/repo/manifests/" + digest.String())
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 404)
|
||||
|
||||
// check/get by tag
|
||||
resp, err = resty.R().Head(BaseURL + "/v2/repo/manifests/test:1.0")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 404)
|
||||
resp, err = resty.R().Get(BaseURL + "/v2/repo/manifests/test:1.0")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 404)
|
||||
So(resp.Body(), ShouldNotBeEmpty)
|
||||
// check/get by reference
|
||||
resp, err = resty.R().Head(BaseURL + "/v2/repo/manifests/" + digest.String())
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 404)
|
||||
resp, err = resty.R().Get(BaseURL + "/v2/repo/manifests/" + digest.String())
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 404)
|
||||
So(resp.Body(), ShouldNotBeEmpty)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
config := api.NewConfig()
|
||||
c := api.NewController(config)
|
||||
dir, err := ioutil.TempDir("", "oci-repo-test")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
//defer os.RemoveAll(dir)
|
||||
c.Config.Storage.RootDirectory = dir
|
||||
go func() {
|
||||
// this blocks
|
||||
if err := c.Run(); err != nil {
|
||||
return
|
||||
}
|
||||
}()
|
||||
for {
|
||||
// poll until ready
|
||||
resp, _ := resty.R().Get(BaseURL)
|
||||
if resp.StatusCode() == 404 {
|
||||
break
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
status := m.Run()
|
||||
ctx := context.Background()
|
||||
_ = c.Server.Shutdown(ctx)
|
||||
os.Exit(status)
|
||||
}
|
27
pkg/cli/BUILD.bazel
Normal file
27
pkg/cli/BUILD.bazel
Normal file
|
@ -0,0 +1,27 @@
|
|||
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["root.go"],
|
||||
importpath = "github.com/anuvu/zot/pkg/cli",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//errors:go_default_library",
|
||||
"//pkg/api:go_default_library",
|
||||
"//pkg/storage:go_default_library",
|
||||
"@com_github_mitchellh_mapstructure//:go_default_library",
|
||||
"@com_github_opencontainers_distribution_spec//:go_default_library",
|
||||
"@com_github_rs_zerolog//log:go_default_library",
|
||||
"@com_github_spf13_cobra//:go_default_library",
|
||||
"@com_github_spf13_viper//:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
timeout = "short",
|
||||
srcs = ["root_test.go"],
|
||||
embed = [":go_default_library"],
|
||||
race = "on",
|
||||
deps = ["@com_github_smartystreets_goconvey//convey:go_default_library"],
|
||||
)
|
99
pkg/cli/root.go
Normal file
99
pkg/cli/root.go
Normal file
|
@ -0,0 +1,99 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"github.com/anuvu/zot/errors"
|
||||
"github.com/anuvu/zot/pkg/api"
|
||||
"github.com/anuvu/zot/pkg/storage"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
dspec "github.com/opencontainers/distribution-spec"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// metadataConfig reports metadata after parsing, which we use to track
|
||||
// errors
|
||||
func metadataConfig(md *mapstructure.Metadata) viper.DecoderConfigOption {
|
||||
return func(c *mapstructure.DecoderConfig) {
|
||||
c.Metadata = md
|
||||
}
|
||||
}
|
||||
|
||||
func NewRootCmd() *cobra.Command {
|
||||
showVersion := false
|
||||
config := api.NewConfig()
|
||||
|
||||
serveCmd := &cobra.Command{
|
||||
Use: "serve <config>",
|
||||
Aliases: []string{"serve"},
|
||||
Short: "`serve` stores and distributes OCI images",
|
||||
Long: "`serve` stores and distributes OCI images",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) > 0 {
|
||||
viper.SetConfigFile(args[0])
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
md := &mapstructure.Metadata{}
|
||||
if err := viper.Unmarshal(&config, metadataConfig(md)); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// if haven't found a single key or there were unused keys, report it as
|
||||
// a error
|
||||
if len(md.Keys) == 0 || len(md.Unused) > 0 {
|
||||
panic(errors.ErrBadConfig)
|
||||
}
|
||||
}
|
||||
c := api.NewController(config)
|
||||
if err := c.Run(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
gcDelUntagged := false
|
||||
gcDryRun := false
|
||||
|
||||
gcCmd := &cobra.Command{
|
||||
Use: "garbage-collect <config>",
|
||||
Aliases: []string{"gc"},
|
||||
Short: "`garbage-collect` deletes layers not referenced by any manifests",
|
||||
Long: "`garbage-collect` deletes layers not referenced by any manifests",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
log.Info().Interface("values", config).Msg("configuration settings")
|
||||
if config.Storage.RootDirectory != "" {
|
||||
if err := storage.Scrub(config.Storage.RootDirectory, gcDryRun); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
gcCmd.Flags().StringVarP(&config.Storage.RootDirectory, "storage-root-dir", "r", "",
|
||||
"Use specified directory for filestore backing image data")
|
||||
_ = gcCmd.MarkFlagRequired("storage-root-dir")
|
||||
gcCmd.Flags().BoolVarP(&gcDelUntagged, "delete-untagged", "m", false,
|
||||
"delete manifests that are not currently referenced via tag")
|
||||
gcCmd.Flags().BoolVarP(&gcDryRun, "dry-run", "d", false,
|
||||
"do everything except remove the blobs")
|
||||
|
||||
rootCmd := &cobra.Command{
|
||||
Use: "zot",
|
||||
Short: "`zot`",
|
||||
Long: "`zot`",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if showVersion {
|
||||
log.Info().Str("version", dspec.Version).Msg("distribution-spec")
|
||||
}
|
||||
_ = cmd.Usage()
|
||||
},
|
||||
}
|
||||
|
||||
rootCmd.AddCommand(serveCmd)
|
||||
rootCmd.AddCommand(gcCmd)
|
||||
rootCmd.Flags().BoolVarP(&showVersion, "version", "v", false, "show the version and exit")
|
||||
|
||||
return rootCmd
|
||||
}
|
42
pkg/cli/root_test.go
Normal file
42
pkg/cli/root_test.go
Normal file
|
@ -0,0 +1,42 @@
|
|||
package cli_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/anuvu/zot/pkg/cli"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestUsage(t *testing.T) {
|
||||
oldArgs := os.Args
|
||||
defer func() { os.Args = oldArgs }()
|
||||
|
||||
Convey("Test Usage", t, func(c C) {
|
||||
os.Args = []string{"cli_test", "help"}
|
||||
err := cli.NewRootCmd().Execute()
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
}
|
||||
|
||||
func TestServe(t *testing.T) {
|
||||
oldArgs := os.Args
|
||||
defer func() { os.Args = oldArgs }()
|
||||
|
||||
Convey("Test Usage", t, func(c C) {
|
||||
os.Args = []string{"cli_test", "serve", "-h"}
|
||||
err := cli.NewRootCmd().Execute()
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGC(t *testing.T) {
|
||||
oldArgs := os.Args
|
||||
defer func() { os.Args = oldArgs }()
|
||||
|
||||
Convey("Test Usage", t, func(c C) {
|
||||
os.Args = []string{"cli_test", "garbage-collect", "-h"}
|
||||
err := cli.NewRootCmd().Execute()
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
}
|
27
pkg/storage/BUILD.bazel
Normal file
27
pkg/storage/BUILD.bazel
Normal file
|
@ -0,0 +1,27 @@
|
|||
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["storage.go"],
|
||||
importpath = "github.com/anuvu/zot/pkg/storage",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//errors:go_default_library",
|
||||
"@com_github_gofrs_uuid//:go_default_library",
|
||||
"@com_github_opencontainers_go_digest//:go_default_library",
|
||||
"@com_github_opencontainers_image_spec//specs-go/v1:go_default_library",
|
||||
"@com_github_rs_zerolog//:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
timeout = "short",
|
||||
srcs = ["storage_test.go"],
|
||||
embed = [":go_default_library"],
|
||||
race = "on",
|
||||
deps = [
|
||||
"@com_github_rs_zerolog//:go_default_library",
|
||||
"@com_github_smartystreets_goconvey//convey:go_default_library",
|
||||
],
|
||||
)
|
704
pkg/storage/storage.go
Normal file
704
pkg/storage/storage.go
Normal file
|
@ -0,0 +1,704 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"sync"
|
||||
|
||||
"github.com/anuvu/zot/errors"
|
||||
guuid "github.com/gofrs/uuid"
|
||||
godigest "github.com/opencontainers/go-digest"
|
||||
ispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
const (
|
||||
BlobUploadDir = ".uploads"
|
||||
)
|
||||
|
||||
type BlobUpload struct {
|
||||
StoreName string
|
||||
ID string
|
||||
}
|
||||
|
||||
type ImageStore struct {
|
||||
rootDir string
|
||||
lock *sync.Mutex
|
||||
blobUploads map[string]BlobUpload
|
||||
log zerolog.Logger
|
||||
}
|
||||
|
||||
func NewImageStore(rootDir string, log zerolog.Logger) *ImageStore {
|
||||
is := &ImageStore{rootDir: rootDir,
|
||||
lock: &sync.Mutex{},
|
||||
blobUploads: make(map[string]BlobUpload),
|
||||
log: log.With().Caller().Logger(),
|
||||
}
|
||||
if _, err := os.Stat(rootDir); os.IsNotExist(err) {
|
||||
_ = os.MkdirAll(rootDir, 0700)
|
||||
} else if _, err := is.Validate(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return is
|
||||
}
|
||||
|
||||
func (is *ImageStore) Validate() (bool, error) {
|
||||
dir := is.rootDir
|
||||
files, err := ioutil.ReadDir(dir)
|
||||
if err != nil {
|
||||
is.log.Error().Err(err).Str("dir", dir).Msg("unable to read directory")
|
||||
return false, errors.ErrRepoNotFound
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
if !file.IsDir() {
|
||||
is.log.Error().Err(err).Str("file", file.Name()).Msg("not a directory")
|
||||
return false, errors.ErrRepoIsNotDir
|
||||
}
|
||||
|
||||
v, err := is.ValidateRepo(file.Name())
|
||||
if !v {
|
||||
return v, err
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (is *ImageStore) InitRepo(name string) error {
|
||||
repoDir := path.Join(is.rootDir, name)
|
||||
|
||||
if fi, err := os.Stat(repoDir); err == nil && fi.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// create repo dir
|
||||
ensureDir(repoDir)
|
||||
|
||||
// create "blobs" subdir
|
||||
dir := path.Join(repoDir, "blobs")
|
||||
ensureDir(dir)
|
||||
|
||||
// create BlobUploadDir subdir
|
||||
dir = path.Join(repoDir, BlobUploadDir)
|
||||
ensureDir(dir)
|
||||
|
||||
// "oci-layout" file - create if it doesn't exist
|
||||
ilPath := path.Join(repoDir, ispec.ImageLayoutFile)
|
||||
if _, err := os.Stat(ilPath); err != nil {
|
||||
il := ispec.ImageLayout{Version: ispec.ImageLayoutVersion}
|
||||
buf, err := json.Marshal(il)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := ioutil.WriteFile(ilPath, buf, 0644); err != nil {
|
||||
is.log.Error().Err(err).Str("file", ilPath).Msg("unable to write file")
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// "index.json" file - create if it doesn't exist
|
||||
indexPath := path.Join(repoDir, "index.json")
|
||||
if _, err := os.Stat(indexPath); err != nil {
|
||||
index := ispec.Index{}
|
||||
index.SchemaVersion = 2
|
||||
buf, err := json.Marshal(index)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := ioutil.WriteFile(indexPath, buf, 0644); err != nil {
|
||||
is.log.Error().Err(err).Str("file", indexPath).Msg("unable to write file")
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (is *ImageStore) ValidateRepo(name string) (bool, error) {
|
||||
// https://github.com/opencontainers/image-spec/blob/master/image-layout.md#content
|
||||
// at least, expect exactly 4 entries - ["blobs", "oci-layout", "index.json"] and BlobUploadDir
|
||||
// in each image store
|
||||
dir := path.Join(is.rootDir, name)
|
||||
if !dirExists(dir) {
|
||||
return false, errors.ErrRepoNotFound
|
||||
}
|
||||
|
||||
files, err := ioutil.ReadDir(dir)
|
||||
if err != nil {
|
||||
is.log.Error().Err(err).Str("dir", dir).Msg("unable to read directory")
|
||||
return false, errors.ErrRepoNotFound
|
||||
}
|
||||
|
||||
if len(files) != 4 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
found := map[string]bool{
|
||||
"blobs": false,
|
||||
ispec.ImageLayoutFile: false,
|
||||
"index.json": false,
|
||||
BlobUploadDir: false,
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
if file.Name() == "blobs" && !file.IsDir() {
|
||||
return false, nil
|
||||
}
|
||||
found[file.Name()] = true
|
||||
}
|
||||
|
||||
for k, v := range found {
|
||||
if !v && k != BlobUploadDir {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
buf, err := ioutil.ReadFile(path.Join(dir, ispec.ImageLayoutFile))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
var il ispec.ImageLayout
|
||||
if err := json.Unmarshal(buf, &il); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if il.Version != ispec.ImageLayoutVersion {
|
||||
return false, errors.ErrRepoBadVersion
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (is *ImageStore) GetRepositories() ([]string, error) {
|
||||
dir := is.rootDir
|
||||
files, err := ioutil.ReadDir(dir)
|
||||
if err != nil {
|
||||
is.log.Error().Err(err).Msg("failure walking storage root-dir")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stores := make([]string, 0)
|
||||
for _, file := range files {
|
||||
p := path.Join(dir, file.Name())
|
||||
is.log.Debug().Str("dir", p).Str("name", file.Name()).Msg("found image store")
|
||||
stores = append(stores, file.Name())
|
||||
}
|
||||
return stores, nil
|
||||
}
|
||||
|
||||
func (is *ImageStore) GetImageTags(repo string) ([]string, error) {
|
||||
dir := path.Join(is.rootDir, repo)
|
||||
if !dirExists(dir) {
|
||||
return nil, errors.ErrRepoNotFound
|
||||
}
|
||||
buf, err := ioutil.ReadFile(path.Join(dir, "index.json"))
|
||||
if err != nil {
|
||||
is.log.Error().Err(err).Str("dir", dir).Msg("failed to read index.json")
|
||||
return nil, errors.ErrRepoNotFound
|
||||
}
|
||||
|
||||
var index ispec.Index
|
||||
if err := json.Unmarshal(buf, &index); err != nil {
|
||||
is.log.Error().Err(err).Str("dir", dir).Msg("invalid JSON")
|
||||
return nil, errors.ErrRepoNotFound
|
||||
}
|
||||
|
||||
tags := make([]string, 0)
|
||||
|
||||
for _, manifest := range index.Manifests {
|
||||
v, ok := manifest.Annotations[ispec.AnnotationRefName]
|
||||
if ok {
|
||||
tags = append(tags, v)
|
||||
}
|
||||
}
|
||||
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
func (is *ImageStore) GetImageManifest(repo string, reference string) ([]byte, string, string, error) {
|
||||
dir := path.Join(is.rootDir, repo)
|
||||
if !dirExists(dir) {
|
||||
return nil, "", "", errors.ErrRepoNotFound
|
||||
}
|
||||
buf, err := ioutil.ReadFile(path.Join(dir, "index.json"))
|
||||
if err != nil {
|
||||
is.log.Error().Err(err).Str("dir", dir).Msg("failed to read index.json")
|
||||
return nil, "", "", err
|
||||
}
|
||||
|
||||
var index ispec.Index
|
||||
if err := json.Unmarshal(buf, &index); err != nil {
|
||||
is.log.Error().Err(err).Str("dir", dir).Msg("invalid JSON")
|
||||
return nil, "", "", err
|
||||
}
|
||||
|
||||
found := false
|
||||
var digest godigest.Digest
|
||||
mediaType := ""
|
||||
for _, m := range index.Manifests {
|
||||
if reference == m.Digest.String() {
|
||||
digest = m.Digest
|
||||
mediaType = m.MediaType
|
||||
found = true
|
||||
break
|
||||
}
|
||||
|
||||
v, ok := m.Annotations[ispec.AnnotationRefName]
|
||||
if ok && v == reference {
|
||||
digest = m.Digest
|
||||
mediaType = m.MediaType
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return nil, "", "", errors.ErrManifestNotFound
|
||||
}
|
||||
|
||||
p := path.Join(dir, "blobs")
|
||||
p = path.Join(p, digest.Algorithm().String())
|
||||
p = path.Join(p, digest.Encoded())
|
||||
|
||||
buf, err = ioutil.ReadFile(p)
|
||||
if err != nil {
|
||||
is.log.Error().Err(err).Str("blob", p).Msg("failed to read manifest")
|
||||
return nil, "", "", err
|
||||
}
|
||||
|
||||
var manifest ispec.Manifest
|
||||
if err := json.Unmarshal(buf, &manifest); err != nil {
|
||||
is.log.Error().Err(err).Str("dir", dir).Msg("invalid JSON")
|
||||
return nil, "", "", err
|
||||
}
|
||||
|
||||
return buf, digest.String(), mediaType, nil
|
||||
}
|
||||
|
||||
func (is *ImageStore) PutImageManifest(repo string, reference string,
|
||||
mediaType string, body []byte) (string, error) {
|
||||
|
||||
if err := is.InitRepo(repo); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if mediaType != ispec.MediaTypeImageManifest {
|
||||
return "", errors.ErrBadManifest
|
||||
}
|
||||
|
||||
if len(body) == 0 {
|
||||
return "", errors.ErrBadManifest
|
||||
}
|
||||
|
||||
var m ispec.Manifest
|
||||
if err := json.Unmarshal(body, &m); err != nil {
|
||||
return "", errors.ErrBadManifest
|
||||
}
|
||||
|
||||
for _, l := range m.Layers {
|
||||
digest := l.Digest
|
||||
blobPath := is.BlobPath(repo, digest)
|
||||
if _, err := os.Stat(blobPath); err != nil {
|
||||
return digest.String(), errors.ErrBlobNotFound
|
||||
}
|
||||
}
|
||||
|
||||
mDigest := godigest.FromBytes(body)
|
||||
refIsDigest := false
|
||||
d, err := godigest.Parse(reference)
|
||||
if err == nil {
|
||||
if d.String() != mDigest.String() {
|
||||
is.log.Error().Str("actual", mDigest.String()).Str("expected", d.String()).
|
||||
Msg("manifest digest is not valid")
|
||||
return "", errors.ErrBadManifest
|
||||
}
|
||||
refIsDigest = true
|
||||
}
|
||||
|
||||
dir := path.Join(is.rootDir, repo)
|
||||
buf, err := ioutil.ReadFile(path.Join(dir, "index.json"))
|
||||
if err != nil {
|
||||
is.log.Error().Err(err).Str("dir", dir).Msg("failed to read index.json")
|
||||
return "", err
|
||||
}
|
||||
|
||||
var index ispec.Index
|
||||
if err := json.Unmarshal(buf, &index); err != nil {
|
||||
is.log.Error().Err(err).Str("dir", dir).Msg("invalid JSON")
|
||||
return "", errors.ErrRepoBadVersion
|
||||
}
|
||||
|
||||
updateIndex := true
|
||||
// create a new descriptor
|
||||
desc := ispec.Descriptor{MediaType: mediaType, Size: int64(len(body)), Digest: mDigest,
|
||||
Platform: &ispec.Platform{Architecture: "amd64", OS: "linux"}}
|
||||
if !refIsDigest {
|
||||
desc.Annotations = map[string]string{ispec.AnnotationRefName: reference}
|
||||
}
|
||||
|
||||
for i, m := range index.Manifests {
|
||||
if reference == m.Digest.String() {
|
||||
// nothing changed, so don't update
|
||||
desc = m
|
||||
updateIndex = false
|
||||
break
|
||||
}
|
||||
|
||||
v, ok := m.Annotations[ispec.AnnotationRefName]
|
||||
if ok && v == reference {
|
||||
if m.Digest.String() == mDigest.String() {
|
||||
// nothing changed, so don't update
|
||||
desc = m
|
||||
updateIndex = false
|
||||
break
|
||||
}
|
||||
// manifest contents have changed for the same tag
|
||||
desc = m
|
||||
desc.Digest = mDigest
|
||||
index.Manifests = append(index.Manifests[:i], index.Manifests[1+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !updateIndex {
|
||||
return desc.Digest.String(), nil
|
||||
}
|
||||
|
||||
// write manifest to "blobs"
|
||||
dir = path.Join(is.rootDir, repo)
|
||||
dir = path.Join(dir, "blobs")
|
||||
dir = path.Join(dir, mDigest.Algorithm().String())
|
||||
_ = os.MkdirAll(dir, 0755)
|
||||
file := path.Join(dir, mDigest.Encoded())
|
||||
if err := ioutil.WriteFile(file, body, 0644); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// now update "index.json"
|
||||
index.Manifests = append(index.Manifests, desc)
|
||||
dir = path.Join(is.rootDir, repo)
|
||||
file = path.Join(dir, "index.json")
|
||||
buf, err = json.Marshal(index)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := ioutil.WriteFile(file, buf, 0644); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return desc.Digest.String(), nil
|
||||
}
|
||||
|
||||
func (is *ImageStore) DeleteImageManifest(repo string, reference string) error {
|
||||
dir := path.Join(is.rootDir, repo)
|
||||
if !dirExists(dir) {
|
||||
return errors.ErrRepoNotFound
|
||||
}
|
||||
buf, err := ioutil.ReadFile(path.Join(dir, "index.json"))
|
||||
if err != nil {
|
||||
is.log.Error().Err(err).Str("dir", dir).Msg("failed to read index.json")
|
||||
return err
|
||||
}
|
||||
|
||||
var index ispec.Index
|
||||
if err := json.Unmarshal(buf, &index); err != nil {
|
||||
is.log.Error().Err(err).Str("dir", dir).Msg("invalid JSON")
|
||||
return err
|
||||
}
|
||||
|
||||
found := false
|
||||
var digest godigest.Digest
|
||||
var i int
|
||||
var m ispec.Descriptor
|
||||
for i, m = range index.Manifests {
|
||||
if reference == m.Digest.String() {
|
||||
digest = m.Digest
|
||||
found = true
|
||||
break
|
||||
}
|
||||
|
||||
v, ok := m.Annotations[ispec.AnnotationRefName]
|
||||
if ok && v == reference {
|
||||
digest = m.Digest
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return errors.ErrManifestNotFound
|
||||
}
|
||||
|
||||
// remove the manifest entry, not preserving order
|
||||
index.Manifests[i] = index.Manifests[len(index.Manifests)-1]
|
||||
index.Manifests = index.Manifests[:len(index.Manifests)-1]
|
||||
|
||||
// now update "index.json"
|
||||
dir = path.Join(is.rootDir, repo)
|
||||
file := path.Join(dir, "index.json")
|
||||
buf, err = json.Marshal(index)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ioutil.WriteFile(file, buf, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p := path.Join(dir, "blobs")
|
||||
p = path.Join(p, digest.Algorithm().String())
|
||||
p = path.Join(p, digest.Encoded())
|
||||
|
||||
_ = os.Remove(p)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (is *ImageStore) BlobUploadPath(repo string, uuid string) string {
|
||||
dir := path.Join(is.rootDir, repo)
|
||||
blobUploadPath := path.Join(dir, BlobUploadDir)
|
||||
blobUploadPath = path.Join(blobUploadPath, uuid)
|
||||
return blobUploadPath
|
||||
}
|
||||
|
||||
func (is *ImageStore) NewBlobUpload(repo string) (string, error) {
|
||||
if err := is.InitRepo(repo); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
uuid, err := guuid.NewV4()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
u := uuid.String()
|
||||
blobUploadPath := is.BlobUploadPath(repo, u)
|
||||
file, err := os.OpenFile(blobUploadPath, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0600)
|
||||
if err != nil {
|
||||
return "", errors.ErrRepoNotFound
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func (is *ImageStore) GetBlobUpload(repo string, uuid string) (int64, error) {
|
||||
blobUploadPath := is.BlobUploadPath(repo, uuid)
|
||||
fi, err := os.Stat(blobUploadPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return -1, errors.ErrUploadNotFound
|
||||
}
|
||||
return -1, err
|
||||
}
|
||||
|
||||
return fi.Size(), nil
|
||||
}
|
||||
|
||||
func (is *ImageStore) PutBlobChunk(repo string, uuid string,
|
||||
from int64, to int64, body io.Reader) (int64, error) {
|
||||
|
||||
if err := is.InitRepo(repo); err != nil {
|
||||
return -1, err
|
||||
}
|
||||
|
||||
blobUploadPath := is.BlobUploadPath(repo, uuid)
|
||||
|
||||
fi, err := os.Stat(blobUploadPath)
|
||||
if err != nil {
|
||||
return -1, errors.ErrUploadNotFound
|
||||
}
|
||||
if from != fi.Size() {
|
||||
is.log.Error().Int64("expected", from).Int64("actual", fi.Size()).
|
||||
Msg("invalid range start for blob upload")
|
||||
return -1, errors.ErrBadUploadRange
|
||||
}
|
||||
|
||||
file, err := os.OpenFile(
|
||||
blobUploadPath,
|
||||
os.O_WRONLY|os.O_CREATE,
|
||||
0600,
|
||||
)
|
||||
if err != nil {
|
||||
is.log.Fatal().Err(err).Msg("failed to open file")
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
if _, err := file.Seek(from, 0); err != nil {
|
||||
is.log.Fatal().Err(err).Msg("failed to seek file")
|
||||
}
|
||||
|
||||
n, err := io.Copy(file, body)
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (is *ImageStore) BlobUploadInfo(repo string, uuid string) (int64, error) {
|
||||
blobUploadPath := is.BlobUploadPath(repo, uuid)
|
||||
fi, err := os.Stat(blobUploadPath)
|
||||
if err != nil {
|
||||
is.log.Error().Err(err).Str("blob", blobUploadPath).Msg("failed to stat blob")
|
||||
return -1, err
|
||||
}
|
||||
size := fi.Size()
|
||||
return size, nil
|
||||
}
|
||||
|
||||
func (is *ImageStore) FinishBlobUpload(repo string, uuid string,
|
||||
body io.Reader, digest string) error {
|
||||
|
||||
dstDigest, err := godigest.Parse(digest)
|
||||
if err != nil {
|
||||
is.log.Error().Err(err).Str("digest", digest).Msg("failed to parse digest")
|
||||
return errors.ErrBadBlobDigest
|
||||
}
|
||||
|
||||
src := is.BlobUploadPath(repo, uuid)
|
||||
|
||||
_, err = os.Stat(src)
|
||||
if err != nil {
|
||||
is.log.Error().Err(err).Str("blob", src).Msg("failed to stat blob")
|
||||
return errors.ErrUploadNotFound
|
||||
}
|
||||
|
||||
f, err := os.Open(src)
|
||||
if err != nil {
|
||||
is.log.Error().Err(err).Str("blob", src).Msg("failed to open blob")
|
||||
return errors.ErrUploadNotFound
|
||||
}
|
||||
srcDigest, err := godigest.FromReader(f)
|
||||
f.Close()
|
||||
if err != nil {
|
||||
is.log.Error().Err(err).Str("blob", src).Msg("failed to open blob")
|
||||
return errors.ErrBadBlobDigest
|
||||
}
|
||||
|
||||
if srcDigest != dstDigest {
|
||||
is.log.Error().Str("srcDigest", srcDigest.String()).
|
||||
Str("dstDigest", dstDigest.String()).Msg("actual digest not equal to expected digest")
|
||||
return errors.ErrBadBlobDigest
|
||||
}
|
||||
|
||||
dir := path.Join(is.rootDir, repo)
|
||||
dir = path.Join(dir, "blobs")
|
||||
dir = path.Join(dir, dstDigest.Algorithm().String())
|
||||
_ = os.MkdirAll(dir, 0755)
|
||||
dst := is.BlobPath(repo, dstDigest)
|
||||
|
||||
// move the blob from uploads to final dest
|
||||
_ = os.Rename(src, dst)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (is *ImageStore) DeleteBlobUpload(repo string, uuid string) error {
|
||||
blobUploadPath := is.BlobUploadPath(repo, uuid)
|
||||
_ = os.Remove(blobUploadPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (is *ImageStore) BlobPath(repo string, digest godigest.Digest) string {
|
||||
dir := path.Join(is.rootDir, repo)
|
||||
blobPath := path.Join(dir, "blobs")
|
||||
blobPath = path.Join(blobPath, digest.Algorithm().String())
|
||||
blobPath = path.Join(blobPath, digest.Encoded())
|
||||
return blobPath
|
||||
}
|
||||
|
||||
func (is *ImageStore) CheckBlob(repo string, digest string,
|
||||
mediaType string) (bool, int64, error) {
|
||||
|
||||
d, err := godigest.Parse(digest)
|
||||
if err != nil {
|
||||
is.log.Error().Err(err).Str("digest", digest).Msg("failed to parse digest")
|
||||
return false, -1, errors.ErrBadBlobDigest
|
||||
}
|
||||
|
||||
blobPath := is.BlobPath(repo, d)
|
||||
|
||||
blobInfo, err := os.Stat(blobPath)
|
||||
if err != nil {
|
||||
is.log.Error().Err(err).Str("blob", blobPath).Msg("failed to stat blob")
|
||||
return false, -1, errors.ErrBlobNotFound
|
||||
}
|
||||
|
||||
return true, blobInfo.Size(), nil
|
||||
}
|
||||
|
||||
// FIXME: we should probably parse the manifest and use (digest, mediaType) as a
|
||||
// blob selector instead of directly downloading the blob
|
||||
func (is *ImageStore) GetBlob(repo string, digest string,
|
||||
mediaType string) (io.Reader, int64, error) {
|
||||
|
||||
d, err := godigest.Parse(digest)
|
||||
if err != nil {
|
||||
is.log.Error().Err(err).Str("digest", digest).Msg("failed to parse digest")
|
||||
return nil, -1, errors.ErrBadBlobDigest
|
||||
}
|
||||
|
||||
blobPath := is.BlobPath(repo, d)
|
||||
|
||||
blobInfo, err := os.Stat(blobPath)
|
||||
if err != nil {
|
||||
is.log.Error().Err(err).Str("blob", blobPath).Msg("failed to stat blob")
|
||||
return nil, -1, errors.ErrBlobNotFound
|
||||
}
|
||||
|
||||
blobReader, err := os.Open(blobPath)
|
||||
if err != nil {
|
||||
is.log.Error().Err(err).Str("blob", blobPath).Msg("failed to open blob")
|
||||
return nil, -1, err
|
||||
}
|
||||
|
||||
return blobReader, blobInfo.Size(), nil
|
||||
}
|
||||
|
||||
func (is *ImageStore) DeleteBlob(repo string, digest string) error {
|
||||
d, err := godigest.Parse(digest)
|
||||
if err != nil {
|
||||
is.log.Error().Err(err).Str("digest", digest).Msg("failed to parse digest")
|
||||
return errors.ErrBlobNotFound
|
||||
}
|
||||
|
||||
blobPath := is.BlobPath(repo, d)
|
||||
|
||||
_, err = os.Stat(blobPath)
|
||||
if err != nil {
|
||||
is.log.Error().Err(err).Str("blob", blobPath).Msg("failed to stat blob")
|
||||
return errors.ErrBlobNotFound
|
||||
}
|
||||
|
||||
_ = os.Remove(blobPath)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// garbage collection
|
||||
|
||||
// TODO
|
||||
|
||||
func Scrub(dir string, fix bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// utility routines
|
||||
|
||||
func dirExists(d string) bool {
|
||||
fi, err := os.Stat(d)
|
||||
if err != nil && os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
if !fi.IsDir() {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func ensureDir(dir string) {
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
48
pkg/storage/storage_test.go
Normal file
48
pkg/storage/storage_test.go
Normal file
|
@ -0,0 +1,48 @@
|
|||
package storage_test
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/anuvu/zot/pkg/storage"
|
||||
"github.com/rs/zerolog"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestRepoLayout(t *testing.T) {
|
||||
dir, err := ioutil.TempDir("", "oci-repo-test")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
il := storage.NewImageStore(dir, zerolog.New(os.Stdout))
|
||||
|
||||
Convey("Repo layout", t, func(c C) {
|
||||
repoName := "test"
|
||||
|
||||
Convey("Validate repo without initialization", func() {
|
||||
v, err := il.ValidateRepo(repoName)
|
||||
So(v, ShouldEqual, false)
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("Initialize repo", func() {
|
||||
err := il.InitRepo(repoName)
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("Validate repo", func() {
|
||||
v, err := il.ValidateRepo(repoName)
|
||||
So(v, ShouldEqual, true)
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("Validate all repos", func() {
|
||||
v, err := il.Validate()
|
||||
So(v, ShouldEqual, true)
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
})
|
||||
}
|
18
test/data/ca.crt
Normal file
18
test/data/ca.crt
Normal file
|
@ -0,0 +1,18 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIC7jCCAdagAwIBAgIJALuTIoaFxZVtMA0GCSqGSIb3DQEBCwUAMAwxCjAIBgNV
|
||||
BAMMASowHhcNMTkwNjIwMDIzNzAwWhcNMjkwNjE3MDIzNzAwWjAMMQowCAYDVQQD
|
||||
DAEqMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx0nMLwovfHblPjVV
|
||||
0EmdUbgvf4Yz0zhFPQn4g7qsXAYl4RoatUxD0Ow6Ovij6UFTCTi2WDiS+ihnLswp
|
||||
ZGlHXmdGtmMnltAL7YAADma5cZhvNEdG2mtGkANZ6IiABVPOU7qHUc3IGCBWbpHK
|
||||
9zywrbv4DN3667C2tFEIt4FNw55uEjpkrF7D7Befc9y4gRPYneGgtWiznQA9vMKi
|
||||
JvOpxBYbVIujz/BWCzNN/Oavbtd3oJUaObXcr4K/jfaMl/Pc5AVx6OxzlptpleMG
|
||||
Lg36dza+ChkQ4FsHJw/O1a8Vp3BIbHzXhQev2dKcXGKUElyEqsxEkh72WYjZMmW4
|
||||
T2V+CwIDAQABo1MwUTAdBgNVHQ4EFgQUEOS5BfVHrqbQjfUYM8MjPgi+k3MwHwYD
|
||||
VR0jBBgwFoAUEOS5BfVHrqbQjfUYM8MjPgi+k3MwDwYDVR0TAQH/BAUwAwEB/zAN
|
||||
BgkqhkiG9w0BAQsFAAOCAQEAPO4r8geI4MufGmaTPE3yRcEfOtZ9d7CTjPYbRyYk
|
||||
g2p/bO2XVUbpfuwo/n2fctddemkqgW8p0SLS0cdFYHW9TzHYUxhL5BWwVkFTz5O8
|
||||
+WrheSkLLR3R4iifNaFL79SEugTH3Alirkz3NjdjPzdql7wHahyxMzPWX+FjYzi1
|
||||
eU+dcKIYjWa/Vs2BUwf2jVC1U7Q+SyoTCjCiyAwfqiwBd3qkiZ3ArxoolfidIArF
|
||||
tA5v6ZHGWP42ZtKxMAz0lfoE3CnjXTVwgtjoIGR0MQ08lPd2PQjtUOMKyYssB2J4
|
||||
v3RmDx5ygZQQHJoR+0oMcLuhkJ8g8O0hS3rSlzU6IN6stA==
|
||||
-----END CERTIFICATE-----
|
28
test/data/ca.key
Normal file
28
test/data/ca.key
Normal file
|
@ -0,0 +1,28 @@
|
|||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDHScwvCi98duU+
|
||||
NVXQSZ1RuC9/hjPTOEU9CfiDuqxcBiXhGhq1TEPQ7Do6+KPpQVMJOLZYOJL6KGcu
|
||||
zClkaUdeZ0a2YyeW0AvtgAAOZrlxmG80R0baa0aQA1noiIAFU85TuodRzcgYIFZu
|
||||
kcr3PLCtu/gM3frrsLa0UQi3gU3Dnm4SOmSsXsPsF59z3LiBE9id4aC1aLOdAD28
|
||||
wqIm86nEFhtUi6PP8FYLM0385q9u13eglRo5tdyvgr+N9oyX89zkBXHo7HOWm2mV
|
||||
4wYuDfp3Nr4KGRDgWwcnD87VrxWncEhsfNeFB6/Z0pxcYpQSXISqzESSHvZZiNky
|
||||
ZbhPZX4LAgMBAAECggEAP+aD2Bl1/HzLKNVFPNI95XQfls5bU8DZQqctzl9O4Pr/
|
||||
rlwGcFeR7y2vxjTvqd1OWMicf1E0n43Q+Apyw0WWosiOvfCxQwRWrsK6QePiVnBA
|
||||
SA0KxQJcz9SjQZJzKkIjCGno9ev72vCThkStRfVp2WtKMCYFTQmOq+bH2r9VRgG3
|
||||
IBjsF2Al2YVSew/SgLVkiflsME3EG50QHNHCzBbQf2q0dDDpROVmsph325THdd9A
|
||||
WJ1BJZD6cxU0WC2Grt0rQP8VrKwRn5nCcR+5buL61hJGPMoMchEUD9qEpaZcSy7J
|
||||
9sV2WPZPFt2ePsIWIO547O3S/f3kCaNt1jLJ7XY3MQKBgQD8VTcS4mMsIDdV0E1X
|
||||
DtwD6ZFPn7K6/x5IDKZ6EyuLrL+pcGg2p9v3r+zHSFQkNRZ5KyKfEZ7D8vgFQOA4
|
||||
H6MkVjnSvZaIYdbKjeSuBnTAoIeVo5CeTKEUCiS6pifhIh8/HVs7rcW4129P3hCr
|
||||
mvbBMIZbwXHq10zn8ATwzJUhFwKBgQDKLzvw1pOQqVyF9hRklS7GwEb7qlxHlx6O
|
||||
3stX7m9yfNnL7qW2CKQTmwxQOatJI/zOgrsXQFTipWZPOcq9eiT8HX6MSiK/0Q0C
|
||||
HJqjHhEgx2TdtbDBkOfmYhtjUfeynRuQ8+qzkSDHjpLk12SutaqYezCXXbyjVLo0
|
||||
7LRAVSDbLQKBgBYK56W5qwomwk63xJnPTX71/2CiRb26HY4TtNNDK3GnJJMLo77q
|
||||
iPepIZkDA36qOI1bLEoTAviBGBN1aGDeuqSo96ImN6kwStAk9w4QuFA/dbinsjFx
|
||||
5jxW6oB3lVJAZdRgnyCmfHg6MZobfv9OqTGVKJeJXYczSZ+VQwk6Bej/AoGAKkMT
|
||||
UXVY5R0xtOLKQngYjfz1GXfz0BcbkRuq/5dcfl7wm7snslQ+D8cSHNbhIem+11/m
|
||||
Qab112Zha2AWK+MTRgvYPvTkLJpDENTv0fbf960WPW3UI7Hpd3O8a9dfYluKvpLt
|
||||
1VkZs/zuYZ1Qc2CP502gy5MRckasoZF04BmrQ4UCgYBK+0m7IJDHy8Mjo/9hf/Jy
|
||||
kcJ21JTvpsl3IqnC5BtpYm/+RRRE4hYczTh/Z0Wlsc2ro2f0U03er72ugjXiJcKl
|
||||
wD0qQT/HcdgY1Suue//IVLKNX/RaO6R4V//+4E7rGbRznPG2iLau7w/j9eaRX4d8
|
||||
YwDdc7C5g8anbO83Ns5xCw==
|
||||
-----END PRIVATE KEY-----
|
1
test/data/ca.srl
Normal file
1
test/data/ca.srl
Normal file
|
@ -0,0 +1 @@
|
|||
93A4FC959A3453F0
|
17
test/data/client.crt
Normal file
17
test/data/client.crt
Normal file
|
@ -0,0 +1,17 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIICqTCCAZECCQCTpPyVmjRT8DANBgkqhkiG9w0BAQsFADAMMQowCAYDVQQDDAEq
|
||||
MB4XDTE5MDYyMDAyMzcwMFoXDTI5MDYxNzAyMzcwMFowITETMBEGA1UECwwKVGVz
|
||||
dENsaWVudDEKMAgGA1UEAwwBKjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
|
||||
ggEBALiJ4GscF/7ZNfRgztdoJ8naCwvlZ8Jk2uf3w7saBsuOCDYFop9ZsmNJ6Sac
|
||||
ds406DmNY/I01JjZYDDE+d4b+a1WF45YXy+O8spQPSlY1sdASCvKU/V/6GPPjt8e
|
||||
UNsCv37tFawpDJrtoWNMWJETBbdNeSoRWHYAhpda70Jyy5te3S9MJkw/y6IRYGQD
|
||||
O8AvpeNPBWkqgor98XcXdMW33NGC8rFeYwp4XkixntEhk+7pVDbgcXf4K/awfpsA
|
||||
OS4eyIssM5Co9rctbmtssYPbbZ31+L67bTGYksrQJaUX0X6qz74xB+0LL4LB2+ww
|
||||
MohJcF5X5mpPO0JvLfJqsj/hXo8CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEArW0g
|
||||
m/eWwO4goZIWcVXc7ndGvH0woBUTdUBiZ4zYwnibXkAYrN037osdY5vrLlLHcZSj
|
||||
qHuHmAnd8N+qcuR+IOQMhPZw6uw/7s+E0N+wro+DnhhzPFfDwFNW7tCKmuuQOlDF
|
||||
bEcUJQOvPF//XdWVn4QoTbe38gqwqbBKG/I7AYm3qZLOUE8F+WxM9wKXk8dEg/4v
|
||||
S1sykCtl0g0EobdJcacQpwMrMJYiiahC63CjQAI9oW9CQgQ0ePH7DI6lwCm3ylt1
|
||||
ZY5AuKsFnzMea6C/0EDP08EpE2EhuAqk0pmZnuQdS1Q9pJg15NoSVJPM8hgnNzrK
|
||||
+TrcrDdPcJ6Zeg2EDQ==
|
||||
-----END CERTIFICATE-----
|
15
test/data/client.csr
Normal file
15
test/data/client.csr
Normal file
|
@ -0,0 +1,15 @@
|
|||
-----BEGIN CERTIFICATE REQUEST-----
|
||||
MIICZjCCAU4CAQAwITETMBEGA1UECwwKVGVzdENsaWVudDEKMAgGA1UEAwwBKjCC
|
||||
ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALiJ4GscF/7ZNfRgztdoJ8na
|
||||
CwvlZ8Jk2uf3w7saBsuOCDYFop9ZsmNJ6Sacds406DmNY/I01JjZYDDE+d4b+a1W
|
||||
F45YXy+O8spQPSlY1sdASCvKU/V/6GPPjt8eUNsCv37tFawpDJrtoWNMWJETBbdN
|
||||
eSoRWHYAhpda70Jyy5te3S9MJkw/y6IRYGQDO8AvpeNPBWkqgor98XcXdMW33NGC
|
||||
8rFeYwp4XkixntEhk+7pVDbgcXf4K/awfpsAOS4eyIssM5Co9rctbmtssYPbbZ31
|
||||
+L67bTGYksrQJaUX0X6qz74xB+0LL4LB2+wwMohJcF5X5mpPO0JvLfJqsj/hXo8C
|
||||
AwEAAaAAMA0GCSqGSIb3DQEBCwUAA4IBAQAFm5BhNj51g+BpU8YRKeFVwhb2XBsC
|
||||
yk5Qp7cV1D60DevFmE3MyzSol6bCSvDbuXRWBI6A6c7ejwlsxMUgScGUinFTMCP0
|
||||
IOiVMGp+hz5Y4ZYi77XAvflz8Rj32Tmu6LnKkQ3GmjXmOoMXapPA874PxfxKb9ho
|
||||
TWaBJ7/6mz4xU/XHZhVn28ijek/wETcACYSsjVK3U52UhSnzjoQMVnkHVgHSIbqE
|
||||
YpfC1TeUBxerMWVDvZRm6vcp/rRvT06tcyRO5SqGBUOmeXzUBCrn7u9QQayu0yAO
|
||||
aHSszx9MEp5uW2Pyq4+LAEP5Q4Ke+7BcjWHm9kF48Ilbfy24Q7O6cGqz
|
||||
-----END CERTIFICATE REQUEST-----
|
28
test/data/client.key
Normal file
28
test/data/client.key
Normal file
|
@ -0,0 +1,28 @@
|
|||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC4ieBrHBf+2TX0
|
||||
YM7XaCfJ2gsL5WfCZNrn98O7GgbLjgg2BaKfWbJjSekmnHbONOg5jWPyNNSY2WAw
|
||||
xPneG/mtVheOWF8vjvLKUD0pWNbHQEgrylP1f+hjz47fHlDbAr9+7RWsKQya7aFj
|
||||
TFiREwW3TXkqEVh2AIaXWu9CcsubXt0vTCZMP8uiEWBkAzvAL6XjTwVpKoKK/fF3
|
||||
F3TFt9zRgvKxXmMKeF5IsZ7RIZPu6VQ24HF3+Cv2sH6bADkuHsiLLDOQqPa3LW5r
|
||||
bLGD222d9fi+u20xmJLK0CWlF9F+qs++MQftCy+CwdvsMDKISXBeV+ZqTztCby3y
|
||||
arI/4V6PAgMBAAECggEAetMqD6BvSCyCgNk+Upj8gpkh6RUTbot6OBLsr8eu5iTu
|
||||
yiYOC0nENdmn2Q8i9DS6rDOzZi5LokBsiYlRVcgA8qHuo8ul7x2R855cVvzOV2gt
|
||||
oRfVsf0kS+qGCXNAFcVKd8yNND1OKoAnftP9zvF+SHbEQn+xBTlsW6kmvm9xnULw
|
||||
f3cffwOLZwV5UFymugBEhJt9EiRVjWJJdVt3f29/ljQg4ZJnnCh8UprtKl73Rkya
|
||||
nVMde6Uq9lD8EyadX6zi3hMSmTO9+qnYIu4rPFdPlE0cVlGRmogMu2FIBVwuZkX3
|
||||
NqppTq3uGdagVP6s6NmZjB2m3/rNulK7M5IghDuogQKBgQDqmBlAajATsabOQo71
|
||||
Zn7bo5v6a1HHqjXIV2wvYM7Mv88zaQb/QMZWdYgSfcJ1e0Ysu6nu6wGpKYiCVvYd
|
||||
E8gV/4xrkiB5Gu7owhMGY2XvNOZks9RycNCEyI6NQ/T5fvjnRlGTJCyhLYnH/645
|
||||
NUjiAiUHBiljDR0itcxSkWvQcQKBgQDJYIbUMYgQJRcRDUD2eKMczpIw3xXiqK0r
|
||||
r0NXE+EENDx5RMz+tf+7RtSRe4+QCsXqgRJXXPCmdrJD74MTZ00sycydjIvIM4Vs
|
||||
0ecAZgB4EwTqq6CrwewMBElqhC8NaiFuamNveQiklsgiUQkWacI2826xrMVltji6
|
||||
d7jag8ee/wKBgQDm3/2qCVd7alERmSt8k/yxSFlPoKMBb6AypOcR0aJ0myjeHbUH
|
||||
LMaFfHIIUMA6QrITgDWDrsEZrIhuTgs1HqzCCZg2nb9bsIgDhkyW8uf0/QjpfpnM
|
||||
bv6oT4ELwh+sE6v+YJQTzXwmu9xnelgKcUhjNV0fho7grp1H9cc6U2fZ4QKBgC17
|
||||
gbhXX5XV6rnNNoj0glK1TUuAd170Hfip4xm9warDaY0yPuKglJvlyYj6UViFNmJa
|
||||
uJvGwAu471ZsuDwfrsyY34AOCFw1VsNXPUdXwm9cTFX8YZOpfvjP1w0Zwc7T060u
|
||||
ljrNKWiTLayihNztEhJ7NNsoXIU2fOWQuM2RyfpdAoGAVOKzRPR5B3DNMXXbzT/m
|
||||
IhmiJ+w+OSgZYL+lejhX4VbV93+LzVsIUez+T/Tqurx9/Pj3SWqJxW6XZFtaL5vZ
|
||||
pPs2k8yysEv27SSQ6mDnotplyLmFiYJY5VLShzGg5LxzoxzH5y5l8D1c/eS+VF+G
|
||||
W493RdVuc7hz1lVxuv2fe6k=
|
||||
-----END PRIVATE KEY-----
|
45
test/data/gen_certs.sh
Executable file
45
test/data/gen_certs.sh
Executable file
|
@ -0,0 +1,45 @@
|
|||
#!/bin/bash -xe
|
||||
|
||||
openssl req \
|
||||
-newkey rsa:2048 \
|
||||
-nodes \
|
||||
-days 3650 \
|
||||
-x509 \
|
||||
-keyout ca.key \
|
||||
-out ca.crt \
|
||||
-subj "/CN=*"
|
||||
|
||||
openssl req \
|
||||
-newkey rsa:2048 \
|
||||
-nodes \
|
||||
-keyout server.key \
|
||||
-out server.csr \
|
||||
-subj "/OU=TestServer/CN=*"
|
||||
|
||||
openssl x509 \
|
||||
-req \
|
||||
-days 3650 \
|
||||
-sha256 \
|
||||
-in server.csr \
|
||||
-CA ca.crt \
|
||||
-CAkey ca.key \
|
||||
-CAcreateserial \
|
||||
-out server.crt \
|
||||
-extfile <(echo subjectAltName = IP:127.0.0.1)
|
||||
|
||||
openssl req \
|
||||
-newkey rsa:2048 \
|
||||
-nodes \
|
||||
-keyout client.key \
|
||||
-out client.csr \
|
||||
-subj "/OU=TestClient/CN=*"
|
||||
|
||||
openssl x509 \
|
||||
-req \
|
||||
-days 3650 \
|
||||
-sha256 \
|
||||
-in client.csr \
|
||||
-CA ca.crt \
|
||||
-CAkey ca.key \
|
||||
-CAcreateserial \
|
||||
-out client.crt
|
1
test/data/htpasswd
Normal file
1
test/data/htpasswd
Normal file
|
@ -0,0 +1 @@
|
|||
test:$2y$05$hlbSXDp6hzDLu6VwACS39ORvVRpr3OMR4RlJ31jtlaOEGnPjKZI1m
|
17
test/data/server.crt
Normal file
17
test/data/server.crt
Normal file
|
@ -0,0 +1,17 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIICwzCCAaugAwIBAgIJAJOk/JWaNFPvMA0GCSqGSIb3DQEBCwUAMAwxCjAIBgNV
|
||||
BAMMASowHhcNMTkwNjIwMDIzNzAwWhcNMjkwNjE3MDIzNzAwWjAhMRMwEQYDVQQL
|
||||
DApUZXN0U2VydmVyMQowCAYDVQQDDAEqMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
|
||||
MIIBCgKCAQEAs8ZX1Qp2w2cQLIUIz7LtOitb3E0gv1zuSg8hsG7TNYydQNi06fF2
|
||||
VpDEGMFau1ZqwtyP6SsjqGYuT78eIHQKMVXnURviv6vp1/5f07LJNy1eLisF/Ng5
|
||||
nkfMR/J4h+yziOeT8CwZfXMLY7u0rti5VqWpV4B8ylGMV79Tz+wXR02xGVQZtcYU
|
||||
K+WNaf0wWZEOQUeHzNCDc46PDsukBvNDMkeDJUy9MnEzLxx/WVYCt/p9xwan/fj+
|
||||
BigSJcG5SzR3MilUEr/pn5PSWgY40Lx8C0W5lnLaO+jaSMSTfhXoCvCLwsgdjA7y
|
||||
6s9nvApL80+Y8Jt8bhCyu2M1vewrblfacQIDAQABoxMwETAPBgNVHREECDAGhwR/
|
||||
AAABMA0GCSqGSIb3DQEBCwUAA4IBAQCIKnzHFciUufTUDIiPYePfmk30XvddOFeT
|
||||
4WUzNhxPxfv1bWX9iefZTsZAEmSDWeE4qMQuJdvICd426sZT5V/VtWcy/a114mjQ
|
||||
At97/Y1GMq+XEnS4295S48QiRjahlZd6N+9X70SnHPqo8YX33+j+8aMorvIpDKVk
|
||||
WBJ0U9prfOhVhm37nHUjemZ/p4oS51XBo79kbXT9tWD63FAAl4SK99/6ZMPXJHoe
|
||||
OuXZdn1X41983z0cV1Ze9QhSgEZum9lCjeGZt8b6s/EhByG3yDoNpDCHtkmk921w
|
||||
a/CH4WZvQe3Q+aFp7tk3XrDPfFuxay2IXE6rXSutYMwiQaZEUs2U
|
||||
-----END CERTIFICATE-----
|
15
test/data/server.csr
Normal file
15
test/data/server.csr
Normal file
|
@ -0,0 +1,15 @@
|
|||
-----BEGIN CERTIFICATE REQUEST-----
|
||||
MIICZjCCAU4CAQAwITETMBEGA1UECwwKVGVzdFNlcnZlcjEKMAgGA1UEAwwBKjCC
|
||||
ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALPGV9UKdsNnECyFCM+y7Tor
|
||||
W9xNIL9c7koPIbBu0zWMnUDYtOnxdlaQxBjBWrtWasLcj+krI6hmLk+/HiB0CjFV
|
||||
51Eb4r+r6df+X9OyyTctXi4rBfzYOZ5HzEfyeIfss4jnk/AsGX1zC2O7tK7YuVal
|
||||
qVeAfMpRjFe/U8/sF0dNsRlUGbXGFCvljWn9MFmRDkFHh8zQg3OOjw7LpAbzQzJH
|
||||
gyVMvTJxMy8cf1lWArf6fccGp/34/gYoEiXBuUs0dzIpVBK/6Z+T0loGONC8fAtF
|
||||
uZZy2jvo2kjEk34V6Arwi8LIHYwO8urPZ7wKS/NPmPCbfG4QsrtjNb3sK25X2nEC
|
||||
AwEAAaAAMA0GCSqGSIb3DQEBCwUAA4IBAQCpj3yysx0u7LRQw9EaSZJhZ92vTnqT
|
||||
KLK1+8GRLLt8obZhq9Iw0s6Q47GRC0dDfu6DwE/sOBPUXXOkdSys+QtqPPHZZPNT
|
||||
JzezflInuATliHGNbXHiQ9Z9uHsbeiiEi604e85mj+m8rf5LOYYGxhTyNN5AONFZ
|
||||
6p1R0IMa/9i8PV6G0JgN0Y8JfGYFuJgVM0Le90bSG0q97W+8Rs7DLQqI//2yV20K
|
||||
PHSRufZoNayh6bVdIimx3ji8/s/VjvI+0hT110RBqUJk8phzZGnKAkiZDMa66weM
|
||||
y8AzuOsLc7TdtxVBGer+ClTSH/VjyuDIqBqxN2hfeB6yD9qWCu1ysvxy
|
||||
-----END CERTIFICATE REQUEST-----
|
28
test/data/server.key
Normal file
28
test/data/server.key
Normal file
|
@ -0,0 +1,28 @@
|
|||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCzxlfVCnbDZxAs
|
||||
hQjPsu06K1vcTSC/XO5KDyGwbtM1jJ1A2LTp8XZWkMQYwVq7VmrC3I/pKyOoZi5P
|
||||
vx4gdAoxVedRG+K/q+nX/l/Tssk3LV4uKwX82DmeR8xH8niH7LOI55PwLBl9cwtj
|
||||
u7Su2LlWpalXgHzKUYxXv1PP7BdHTbEZVBm1xhQr5Y1p/TBZkQ5BR4fM0INzjo8O
|
||||
y6QG80MyR4MlTL0ycTMvHH9ZVgK3+n3HBqf9+P4GKBIlwblLNHcyKVQSv+mfk9Ja
|
||||
BjjQvHwLRbmWcto76NpIxJN+FegK8IvCyB2MDvLqz2e8CkvzT5jwm3xuELK7YzW9
|
||||
7CtuV9pxAgMBAAECggEAVKyTKhDnp1mf0JhIciuAeOl7NuRNDFUlF1TRNVy9tnco
|
||||
iiaH77h/WH6PHmnT5nDpkCZ60gQzo1mdbopCEl8Vfe9MKHPN9SFv3wA8+mU3SPnh
|
||||
ZjV1eIYPfXGr0iduhfcDCPSqRXFAAEpzjuIWVFRX12vnuwMVw+VtCNdhDonQ3Q/8
|
||||
jpGi1LDjadckmDkf9QbHBiec9Me/oXd18R9npK9yp8zJCvLUhVeWHdFl1YTvK8QE
|
||||
s9/IffRO/CLofie4VvR4lLT02Hj47jgMfuKyF0Y+qDykT2AxJsBpdIIMy21hLDTp
|
||||
RoHHbzJlcwL9ITzas/daVWHqFADSvyK7ZfWggxjgAQKBgQDg09Qw3hN98Deo6fsA
|
||||
rcn1BDflDHLEc0hY/L/NqLb0EnUMYKZSGI9QbyZP3Oh3jG2G/WdOeq9QLpEIrauF
|
||||
kd5BTDBRgjx0YzwqIu6rv0vwdo5a7+TATETTGH1gZUTmno3yL2b2OdTA33ewyX7o
|
||||
rwDEYaTg4ACJLwPqT+vwJCaugQKBgQDMs2KjjpXkEZgTz4tbcTQsNL7ulTOcwYR7
|
||||
mOsntXTPHSxB9UiTLFvvgo+/okoCUtW1qztDGzdCjilLNc3lcgpHvGS+pX9MtFKo
|
||||
lsVnw8cUM7kGHEAjoauGCVYmaZNuOCcbhWvaQEPo8424TkC29PCZNHbC6n5gBQMV
|
||||
ndQfnfoT8QKBgQC54WkGHhWvgfQCy7CilwzqblpoHSqmEUo3iIBr4Jmiob/0Q9Q+
|
||||
+99BeSQL03C/pnLHsKrAz94yRM3UhwHQpRFEm2E3gp3I/GK507fQd5Cpdturg7t0
|
||||
4ZnljdHa6N9WbLCfE2HlIVstO5URrQYoCshvlOtkoM7QnPZ3uywulzUEAQKBgF4g
|
||||
vuLm1hYh4QR7E2HhFFSfjIy5HxqeAgWzs652ylfS2l8aI11JsJzaNK+yOMYIwSzg
|
||||
qEebZDW+mU50V1GCtyd1gf4IrBjhcoEDk5K7e/fWMOaWZwf7d5wS/wJ62ch9Gb6W
|
||||
A5pAovmjxS9TDH8U8u4AKfxHSAVvSJPQF5LSWgSBAoGBANbFPrVXgcmCxHRAq9U4
|
||||
tybOgJuU1MkGHQBW6i3bQZqxBu2A+h7ORBp/mFZzFKUrxaG8YrBqfiQOznQnPLyZ
|
||||
k0C4sWPSF7CDD9ZjVS86yOYRzBVlCFWSaGttii2rFuuSEdDjPUOoUhO1NcKSevm1
|
||||
KqLTO/4DvBVib2nMAPzTt1pZ
|
||||
-----END PRIVATE KEY-----
|
1
zot.go
Normal file
1
zot.go
Normal file
|
@ -0,0 +1 @@
|
|||
package zot
|
Loading…
Reference in a new issue