Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 9708f19

Browse files
authored
Merge pull request #1 from bvanelli/features/new-features
feat: Introduce new features to CRPY
2 parents b71a55d + ddccfa3 commit 9708f19

7 files changed

Lines changed: 223 additions & 57 deletions

File tree

README.md

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,27 +14,34 @@ since received so many changes that it does not resemble the original code anymo
1414
TODO: Fill in once the "final" version of the API is stable. For a preview of the options, here is the help command:
1515

1616
```
17-
>>> crpy --help
18-
usage: crpy [-h] [-k] [-p PROXY] {pull,push,login,inspect,repositories,tags,delete} ...
17+
usage: crpy [-h] [-k] [-p PROXY]
18+
{pull,push,login,logout,auth,manifest,config,commands,layer,repositories,tags,delete} ...
1919
2020
Package that can do basic docker command like pull and push without installing the docker virtual machine
2121
2222
positional arguments:
23-
{pull,push,login,inspect,repositories,tags,delete}
23+
{pull,push,login,logout,auth,manifest,config,commands,layer,repositories,tags,delete}
2424
pull Pulls a docker image from a remove repo.
2525
push Pushes a docker image from a remove repo.
2626
login Logs in on a remote repo
27-
inspect Inspects a docker registry metadata. It can inspect configs, manifests and layers.
27+
logout Logs out of a remote repo
28+
auth Shows authenticated repositories
29+
manifest Inspects a docker registry metadata.
30+
config Inspects a docker registry metadata.
31+
commands Inspects a docker registry build commands. These are the same as when you check individual
32+
image layers on Docker hub.
33+
layer Inspects a docker registry layer.
2834
repositories List the repositories on the registry.
2935
tags List the tags on a repository.
3036
delete Deletes a tag in a remote repo.
3137
32-
options:
38+
optional arguments:
3339
-h, --help show this help message and exit
34-
-k, --insecure Use insecure registry. Ignores the validation of the certificate (useful for
35-
development registries).
40+
-k, --insecure Use insecure registry. Ignores the validation of the certificate (useful for development
41+
registries).
3642
-p PROXY, --proxy PROXY
37-
Proxy for all requests.
43+
Proxy for all requests. If your proxy contains authentication, pass it on the request in the
44+
usual format "http://user:[email protected]"
3845
3946
For reporting issues visit https://github.com/bvanelli/crpy
4047
```

crpy/cmd.py

Lines changed: 103 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,33 @@
11
import argparse
22
import asyncio
33
import json
4-
import os
54
import sys
65
from getpass import getpass
76

87
from rich import print
8+
from rich.table import Table
99

1010
from crpy.common import HTTPConnectionError, UnauthorizedError
1111
from crpy.registry import RegistryInfo
12-
from crpy.storage import save_credentials
12+
from crpy.storage import (
13+
decode_credentials,
14+
get_config,
15+
remove_credentials,
16+
save_credentials,
17+
)
1318

1419

1520
async def _pull(args):
16-
ri = RegistryInfo.from_url(args.url[0])
21+
ri = RegistryInfo.from_url(args.url[0], proxy=args.proxy, insecure=args.insecure)
1722
filename = args.filename
1823
if not filename:
1924
# make file name compatible
2025
filename = ri.repository.replace(":", "_").replace("/", "_")
21-
await ri.pull(filename)
26+
await ri.pull(filename, args.architecture[0] if args.architecture else None)
2227

2328

2429
async def _push(args):
25-
ri = RegistryInfo.from_url(args.url[0])
30+
ri = RegistryInfo.from_url(args.url[0], proxy=args.proxy, insecure=args.insecure)
2631
await ri.push(args.filename[0])
2732

2833

@@ -31,19 +36,38 @@ async def _login(args):
3136
args.username = input("Username: ")
3237
if args.password is None:
3338
args.password = getpass("Password: ")
34-
ri = RegistryInfo.from_url(args.url)
39+
ri = RegistryInfo.from_url(args.url, proxy=args.proxy, insecure=args.insecure)
3540
await ri.auth(username=args.username, password=args.password)
3641
save_credentials(ri.registry, args.username, args.password)
3742

3843

44+
async def _logout(args):
45+
ri = RegistryInfo.from_url(args.url)
46+
assert not ri.repository, (
47+
"Invalid url provided. Please provide the full registry url, without repository name.\n"
48+
" Example: [bold]index.docker.io[/bold] instead of [bold]index.docker.io/library/alpine[/bold]\n"
49+
" [bold]http://localhost:5000[/bold] instead of [bold]localhost:5000[/bold]"
50+
)
51+
if remove_credentials(ri.registry):
52+
print(f"Removed credentials for {ri.registry}")
53+
else:
54+
raise ValueError(f"Could find find credentials for {ri.registry}")
55+
56+
3957
async def _inspect_manifest(args):
40-
ri = RegistryInfo.from_url(args.url[0])
41-
manifest = await ri.get_manifest_from_architecture()
58+
ri = RegistryInfo.from_url(args.url[0], proxy=args.proxy, insecure=args.insecure)
59+
if args.fat and args.architecture:
60+
raise ValueError("Cannot provide --fat and --architecture together.")
61+
if args.fat:
62+
manifest_raw = await ri.get_manifest(fat=True)
63+
manifest = manifest_raw.json()
64+
else:
65+
manifest = await ri.get_manifest_from_architecture(args.architecture[0] if args.architecture else None)
4266
print(manifest)
4367

4468

4569
async def _inspect_config(args):
46-
ri = RegistryInfo.from_url(args.url[0])
70+
ri = RegistryInfo.from_url(args.url[0], proxy=args.proxy, insecure=args.insecure)
4771
raw_config = await ri.get_config()
4872
config = json.loads(raw_config.data)
4973
if not args.short:
@@ -54,7 +78,7 @@ async def _inspect_config(args):
5478

5579

5680
async def _inspect_layer(args):
57-
ri = RegistryInfo.from_url(args.url[0])
81+
ri = RegistryInfo.from_url(args.url[0], proxy=args.proxy, insecure=args.insecure)
5882
layers = await ri.get_layers()
5983
ref = args.layer_reference[0]
6084
try:
@@ -69,27 +93,43 @@ async def _inspect_layer(args):
6993

7094

7195
async def _repositories(args):
72-
ri = RegistryInfo.from_url(args.url[0])
96+
ri = RegistryInfo.from_url(args.url[0], proxy=args.proxy, insecure=args.insecure)
7397
for entry in await ri.list_repositories():
7498
print(entry)
7599

76100

77101
async def _tags(args):
78-
ri = RegistryInfo.from_url(args.url[0])
102+
ri = RegistryInfo.from_url(args.url[0], proxy=args.proxy, insecure=args.insecure)
79103
if not ri.repository:
80104
raise ValueError("Repository must be provided to list tags!")
81105
for entry in await ri.list_tags():
82106
print(entry)
83107

84108

85109
async def _delete(args):
86-
ri = RegistryInfo.from_url(args.url[0])
110+
ri = RegistryInfo.from_url(args.url[0], proxy=args.proxy, insecure=args.insecure)
87111
if not ri.repository:
88112
raise ValueError("Repository must be provided to list tags!")
89113
r = await ri.delete_tag()
90114
print(r.data)
91115

92116

117+
async def _auth(args):
118+
config = get_config()
119+
120+
table = Table(title="Saved credentials", title_style="bold")
121+
table.add_column("Index", style="blue")
122+
table.add_column("Url", style="cyan", no_wrap=True)
123+
table.add_column("Username", style="magenta")
124+
table.add_column("Password", style="green")
125+
for idx, (url, entry) in enumerate(config["auths"].items()):
126+
username, password = decode_credentials(entry["auth"])
127+
if not args.show_passwords:
128+
password = f"{password[0:2]}***{password[-2:]}"
129+
table.add_row(str(idx), url, username, password)
130+
print(table)
131+
132+
93133
def main(*args):
94134
parser = argparse.ArgumentParser(
95135
prog="crpy",
@@ -102,15 +142,31 @@ def main(*args):
102142
"--insecure",
103143
action="store_true",
104144
help="Use insecure registry. Ignores the validation of the certificate (useful for development registries).",
145+
default=False,
146+
)
147+
parser.add_argument(
148+
"-p",
149+
"--proxy",
150+
nargs=1,
151+
help="Proxy for all requests. If your proxy contains authentication, pass it on the request in the usual "
152+
'format "http://user:[email protected]"',
105153
default=None,
106154
)
107-
parser.add_argument("-p", "--proxy", nargs=1, help="Proxy for all requests.", default=None)
108155
subparsers = parser.add_subparsers()
109156
pull = subparsers.add_parser(
110157
"pull",
111158
help="Pulls a docker image from a remove repo.",
112159
)
113160
pull.set_defaults(func=_pull)
161+
pull.add_argument(
162+
"--architecture",
163+
"-a",
164+
"--arch",
165+
"--platform",
166+
nargs=1,
167+
help="Architecture for the to be pulled.",
168+
default=None,
169+
)
114170
pull.add_argument("url", nargs=1, help="Remote repository to pull from.")
115171
pull.add_argument("filename", nargs="?", help="Output file for the compressed image.")
116172

@@ -122,6 +178,7 @@ def main(*args):
122178
push.add_argument("filename", nargs=1, help="File containing the docker image to be pushed.")
123179
push.add_argument("url", nargs=1, help="Remote repository to push to.")
124180

181+
# authentication
125182
login = subparsers.add_parser("login", help="Logs in on a remote repo")
126183
login.set_defaults(func=_login)
127184
login.add_argument(
@@ -133,29 +190,53 @@ def main(*args):
133190
login.add_argument("--username", "-u", nargs="?", help="Username", default=None)
134191
login.add_argument("--password", "-p", nargs="?", help="Password", default=None)
135192

136-
inspect = subparsers.add_parser(
137-
"inspect",
138-
help="Inspects a docker registry metadata. It can inspect configs, manifests and layers.",
193+
logout = subparsers.add_parser("logout", help="Logs out of a remote repo")
194+
logout.add_argument("url", nargs="?", help="Remote repository to logout from.", default="index.docker.io")
195+
logout.set_defaults(func=_logout)
196+
197+
auth = subparsers.add_parser("auth", help="Shows authenticated repositories")
198+
auth.add_argument(
199+
"--show-passwords",
200+
"-s",
201+
action="store_true",
202+
default=False,
203+
help="If the password or token should be shown in clear text.",
139204
)
140-
inspect_subparser = inspect.add_subparsers()
205+
auth.set_defaults(func=_auth)
206+
141207
# manifest
142-
manifest = inspect_subparser.add_parser("manifest", help="Inspects a docker registry metadata.")
208+
manifest = subparsers.add_parser("manifest", help="Inspects a docker registry metadata.")
209+
manifest.add_argument(
210+
"--fat",
211+
"-f",
212+
action="store_true",
213+
help="If should retrieve the fat manifest, with all different architechtures .",
214+
)
215+
manifest.add_argument(
216+
"--architecture",
217+
"-a",
218+
"--arch",
219+
"--platform",
220+
nargs=1,
221+
help="Architecture to retrieve the manifest for.",
222+
default=None,
223+
)
143224
manifest.add_argument("url", nargs=1, help="Remote repository url.")
144225
manifest.set_defaults(func=_inspect_manifest)
145226
# config
146-
config = inspect_subparser.add_parser("config", help="Inspects a docker registry metadata.")
227+
config = subparsers.add_parser("config", help="Inspects a docker registry metadata.")
147228
config.add_argument("url", nargs=1, help="Remote repository url.")
148229
config.set_defaults(func=_inspect_config, short=False)
149230
# commands
150-
commands = inspect_subparser.add_parser(
231+
commands = subparsers.add_parser(
151232
"commands",
152233
help="Inspects a docker registry build commands. "
153234
"These are the same as when you check individual image layers on Docker hub.",
154235
)
155236
commands.add_argument("url", nargs=1, help="Remote repository url.")
156237
commands.set_defaults(func=_inspect_config, short=True)
157238
# layer
158-
layer = inspect_subparser.add_parser("layer", help="Inspects a docker registry layer.")
239+
layer = subparsers.add_parser("layer", help="Inspects a docker registry layer.")
159240
layer.add_argument("url", nargs=1, help="Remote repository url.")
160241
layer.add_argument(
161242
"layer_reference",
@@ -182,10 +263,6 @@ def main(*args):
182263

183264
arguments = parser.parse_args(args if args else None)
184265

185-
# if a proxy is set, use it on env variables
186-
if arguments.proxy:
187-
os.environ["HTTP_PROXY"] = os.environ["HTTPS_PROXY"] = arguments.proxy
188-
189266
try:
190267
if not hasattr(arguments, "func"):
191268
parser.print_help()

crpy/common.py

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ async def _request(
2828
) -> Response:
2929
aiohttp_kwargs = aiohttp_kwargs or {}
3030
try:
31-
async with aiohttp.ClientSession(trust_env=True) as session:
31+
async with aiohttp.ClientSession() as session:
3232
method_fn = getattr(session, method)
3333
async with method_fn(url, headers=headers, params=params, data=data, **aiohttp_kwargs) as response:
3434
return Response(response.status, await response.read(), dict(response.headers))
@@ -64,11 +64,39 @@ def compute_sha256(file: Union[str, io.BytesIO, bytes]):
6464

6565

6666
class Platform(enum.Enum):
67+
# taken from https://github.com/docker-library/bashbrew/blob/v0.1.2/architecture/oci-platform.go#L14-L27
6768
LINUX = "linux/amd64"
6869
MAC = "linux/arm64/v8"
69-
70-
71-
def platform_from_dict(platform: dict):
70+
WINDOWS = "windows/amd64"
71+
# less used platforms
72+
ARM_32_V5 = "linux/arm/v5"
73+
ARM_32_V6 = "linux/arm/v6"
74+
ARM_32_V7 = "linux/arm/v7"
75+
I386 = "linux/386"
76+
MIPS64lE = "linux/mips64le"
77+
PPC64LE = "linux/ppc64le"
78+
RISCV64 = "linux/riscv64"
79+
S390X = "linux/s390x"
80+
81+
@classmethod
82+
def from_dict(cls, platform: dict) -> "Platform":
83+
return cls(platform_from_dict(platform))
84+
85+
@property
86+
def os(self) -> str:
87+
return self.value.split("/")[0]
88+
89+
@property
90+
def architecture(self) -> str:
91+
return self.value.split("/")[1]
92+
93+
@property
94+
def variant(self) -> Optional[str]:
95+
split_value = self.value.split("/")
96+
return split_value[2] if len(split_value) > 2 else None
97+
98+
99+
def platform_from_dict(platform: dict) -> str:
72100
base_str = f"{platform.get('os')}/{platform.get('architecture')}"
73101
if "variant" in platform:
74102
base_str += f"/{platform.get('variant')}"

0 commit comments

Comments
 (0)