Skip to content

Commit

Permalink
feature: 实现镜像操作
Browse files Browse the repository at this point in the history
  • Loading branch information
shabbywu committed Dec 28, 2021
1 parent 5eb8c0d commit 80f8a42
Show file tree
Hide file tree
Showing 17 changed files with 740 additions and 121 deletions.
30 changes: 25 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Or install from GitHub for latest version.
```

### Introduction
The API provides several classes: `ManifestRef`, `Blob`, `Tags`, `DockerRegistryV2Client`, `APIEndpoint`
The API provides several classes: `ManifestRef`, `Blob`, `Tags`, `DockerRegistryV2Client`, `APIEndpoint`, `ImageRef`

`ManifestRef` has the following methods:
- `get(media_type)` retrieve image manifest as the provided media_type
Expand All @@ -40,6 +40,13 @@ The API provides several classes: `ManifestRef`, `Blob`, `Tags`, `DockerRegistry
- `get(tag)` retrieve the manifest descriptor identified by the tag.
- `untag(tag)` work like `ManifestRef.delete()`

`ImageRef` has the following methods:
- `from_image(from_repo, from_reference, to_repo, to_reference)` init a `ImageRef` from `{from_repo}:{from_reference}` but will name `{to_repo, to_reference}`.
- `save(dest)` save the image to dest, as Docker Image Specification v1.2 Format.
- `push(media_type="application/vnd.docker.distribution.manifest.v2+json")` push the image to the registry.
- `push_v2()` push the image to the registry, with Manifest Schema2.
- `add_layer(layer_ref)` add a layer to this image, this is a way to build a new Image.

`DockerRegistryV2Client` has the following methods:
- `from_api_endpoint(api_endpoint, username, password)` initial a client to the `api_endpoint` with `username` and `password`

Expand All @@ -53,15 +60,15 @@ APIEndpoint(url="https://registry.hub.docker.com")
APIEndpoint(url="registry.hub.docker.com")
```

if the scheme is missing, we will detect whether the server provides ssl and verify the certificate.
if the scheme is missing, we will detect whether the server provides ssl and verify the certificate.

If no ssl: use http(80).
If have ssl, but certificate is invalid:
If have ssl, but certificate is invalid:
- try to ping the registry with https(443), if success, use it
- otherwise, downgrade to http(80)
If have ssl and valid certificate: use https(443)

We provide an anonymous client connected to Docker Official Registry as default, you can find it at `moby_distribution.default_client`,
We provide an anonymous client connected to Docker Official Registry as default, you can find it at `moby_distribution.default_client`,
and you can override the default client by `set_default_client(client)`.

### Example
Expand Down Expand Up @@ -150,7 +157,20 @@ Read the process description of [the official document](https://github.com/distr

Done, Congratulations!

**Here is another way, use the newly implemented ImageRef!**
```python
from moby_distribution import ImageRef, DockerRegistryV2Client, OFFICIAL_ENDPOINT

# To upload files to Docker Registry, you must login to your account
client = DockerRegistryV2Client.from_api_endpoint(OFFICIAL_ENDPOINT, username="your-username", password="your-password")

image_ref = ImageRef.from_image(from_repo="your-repo", from_reference="your-reference", to_reference="the-new-reference")
image_ref.push()
```
The above statement achieves the equivalent function of `docker tag {your-repo}:{your-reference} {your-repo}:{the-new-reference} && docker push {your-repo}:{the-new-reference}`

### RoadMap
- [x] implement the Distribution Client API for moby(docker)
- [ ] Command line tool for operating Image(Tools that implement Example 6)
- [x] implement the Docker Image Operator(Operator that implement Example 6)
- [ ] Command line tool for operating Image
- [ ] implement the Distribution Client API for OCI
10 changes: 8 additions & 2 deletions moby_distribution/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from moby_distribution.registry.client import DockerRegistryV2Client, default_client
from moby_distribution.registry.client import DockerRegistryV2Client, default_client, set_default_client
from moby_distribution.registry.resources.blobs import Blob
from moby_distribution.registry.resources.image import ImageRef, LayerRef
from moby_distribution.registry.resources.manifests import ManifestRef
from moby_distribution.registry.resources.tags import Tags
from moby_distribution.spec.endpoint import OFFICIAL_ENDPOINT, APIEndpoint
from moby_distribution.spec.image_json import ImageJSON
from moby_distribution.spec.manifest import ManifestSchema1, ManifestSchema2, OCIManifestSchema1

__version__ = "0.2.1"
__version__ = "0.3.0"
__ALL__ = [
"DockerRegistryV2Client",
"Blob",
Expand All @@ -15,5 +17,9 @@
"ManifestSchema1",
"ManifestSchema2",
"OCIManifestSchema1",
"ImageJSON",
"ImageRef",
"LayerRef",
"default_client",
"set_default_client",
]
4 changes: 1 addition & 3 deletions moby_distribution/registry/resources/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
from typing import Optional

from moby_distribution.registry.client import DockerRegistryV2Client, default_client


class RepositoryResource:
def __init__(
self,
repo: str,
client: Optional[DockerRegistryV2Client] = default_client,
client: DockerRegistryV2Client = default_client,
):
self.repo = repo
if client is not None:
Expand Down
77 changes: 63 additions & 14 deletions moby_distribution/registry/resources/blobs.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import hashlib
import shutil
from contextlib import contextmanager
from pathlib import Path
from typing import BinaryIO, Optional, Tuple, Union
Expand All @@ -7,6 +8,7 @@
from moby_distribution.registry import exceptions
from moby_distribution.registry.client import DockerRegistryV2Client, URLBuilder, default_client
from moby_distribution.registry.resources import RepositoryResource
from moby_distribution.spec.base import Descriptor


class Blob(RepositoryResource):
Expand All @@ -33,6 +35,23 @@ def accessor(self):
self._accessor = Accessor(local_path=self.local_path, fileobj=self.fileobj)
return self._accessor

def stat(self, digest: Optional[str] = None) -> Descriptor:
"""Obtain resource information without receiving all data."""
digest = digest or self.digest
if digest is None:
raise RuntimeError("unknown digest")

url = URLBuilder.build_blobs_url(self.client.api_base_url, repo=self.repo, digest=digest)
resp = self.client.head(url=url)
headers = resp.headers
return Descriptor(
# Content-Type: application/octet-stream
mediaType=headers["Content-Type"],
size=headers["Content-Length"],
digest=headers["Docker-Content-Digest"],
urls=[url],
)

def download(self, digest: Optional[str] = None):
"""download the blob from registry to `local_path` or `fileobj`"""
digest = digest or self.digest
Expand All @@ -45,23 +64,21 @@ def download(self, digest: Optional[str] = None):
for chunk in resp.iter_content(chunk_size=1024):
fh.write(chunk)

def upload(self) -> bool:
def upload(self) -> Descriptor:
"""upload the blob from `local_path` or `fileobj` to the registry by streaming"""
uuid, location = self._initiate_blob_upload()
blob = BlobWriter(uuid, location, client=self.client)
sha256 = hashlib.sha256()
with self.accessor.open(mode="rb") as fh:
chunk = fh.read(1024 * 1024 * 4)
sha256.update(chunk)
blob.write(chunk)
signer = HashSigner(fh=blob) # type: ignore
shutil.copyfileobj(fsrc=fh, fdst=signer, length=1024 * 1024 * 4)

digest = signer.digest()
blob.commit(digest)

digest = f"sha256:{sha256.hexdigest()}"
if blob.commit(digest):
self.digest = digest
return True
return False
self.digest = digest
return self.stat()

def upload_at_one_time(self):
def upload_at_one_time(self) -> Descriptor:
"""upload the monolithic blob from `local_path` or `fileobj` to the registry at one time."""
data = self.accessor.read_bytes()
digest = f"sha256:{hashlib.sha256(data).hexdigest()}"
Expand All @@ -75,7 +92,7 @@ def upload_at_one_time(self):
if resp.status_code != 201:
raise exceptions.RequestErrorWithResponse("failed to upload", status_code=resp.status_code, response=resp)
self.digest = digest
return True
return self.stat()

def _initiate_blob_upload(self) -> Tuple[str, str]:
"""Initiate a resumable blob upload.
Expand Down Expand Up @@ -104,7 +121,7 @@ def _initiate_blob_upload(self) -> Tuple[str, str]:
location = f"{self.client.api_base_url}/{location.lstrip('/')}"
return uuid, location

def mount_from(self, from_repo: str) -> bool:
def mount_from(self, from_repo: str) -> Descriptor:
"""Mount the blob from the given repo, if the client has read access to."""
if self.digest is None:
raise RuntimeError("unknown digest")
Expand All @@ -117,7 +134,7 @@ def mount_from(self, from_repo: str) -> bool:
raise exceptions.RequestErrorWithResponse(
f"failed to mount blob({self.digest}) from `{from_repo}`", status_code=resp.status_code, response=resp
)
return True
return self.stat()

def delete(self, digest: Optional[str] = None):
"""Delete the blob identified by repo and digest"""
Expand Down Expand Up @@ -177,6 +194,9 @@ def commit(self, digest: str) -> bool:
self._committed = True
return True

def tell(self) -> int:
return self._offset


class Accessor:
def __init__(self, local_path: Optional[Path] = None, fileobj: Optional[BinaryIO] = None):
Expand All @@ -200,3 +220,32 @@ def read_bytes(self):
self.fileobj.seek(0)
return self.fileobj.read()
return self.local_path.read_bytes()


class CounterIO:
def __init__(self):
self.size = 0

def write(self, chunk: bytes):
self.size += len(chunk)
return len(chunk)

def tell(self) -> int:
return self.size


class HashSigner(BinaryIO):
def __init__(self, fh: Union[BinaryIO, CounterIO] = CounterIO(), constructor=hashlib.sha256):
self._raw_fh = fh
self.signer = constructor()

def write(self, chunk: bytes):
self.signer.update(chunk)
return self._raw_fh.write(chunk)

def tell(self) -> int:
return self._raw_fh.tell()

def digest(self) -> str:
"""return hexdigest with hash method name"""
return f"{self.signer.name}:{self.signer.hexdigest()}"
Loading

0 comments on commit 80f8a42

Please sign in to comment.