Loading README.md +18 −7 Original line number Diff line number Diff line Loading @@ -31,20 +31,31 @@ To instantiate it, create a container environment, then a base container with that environment. Then apply the action. from contaminate import Container, ContainerEnv, apply from contaminate import Container, ContainerEnv env = ContainerEnv(system="localhost", arch="linux/x86_64", bind=["/home"]) c = Container(name="ubuntu:16.04", env=env, value="") c2 = apply(c, build_essentials) c2 = c.run(build_essentials) print(c2.name) print(c2.name) # ubuntu.1 You can also run commands directly in the container You can also chain/compose commands with `apply`, run commands directly in the container using the Shell action, and copy files from the container using the Download action, apply(c2, Shell(script="w")) apply(c2, Download(src="/etc/hosts", dst="hostfile")) steps = Shell(script="w") \ .apply(Download(src="/etc/hosts", dst="hostfile")) c2.run(steps) The coontainer build process makes use of `podman build`, so the images are stored based on podman's conventions. You can save and load the extended container environment information using: c2.write("my_container.json") c2 = Container.read("my_container.json") contaminate/__init__.py +1 −1 Original line number Diff line number Diff line from .container import Container from .container_env import ContainerEnv from .action import Return, Script, Shell, Upload, Download, applyM from .action import Return, Script, Shell, Upload, Download contaminate/action.py +53 −20 Original line number Diff line number Diff line from enum import Enum from typing import Literal, Union, Callable from typing_extensions import Annotated from pathlib import Path from pydantic import BaseModel, Field Loading @@ -8,29 +9,29 @@ from .container import Container from .container_env import ContainerEnv from .podman import runcmd class Return(BaseModel): action_type : Literal["return"] class AReturn(BaseModel): action_type : Literal["return"] = "return" value : str def __call__(self, c : Container) -> Container: return c.with_value(self.value) class Script(BaseModel): action_type : Literal["script"] class AScript(BaseModel): action_type : Literal["script"] = "script" script : str def __call__(self, c : Container) -> Container: return c.run_cmd(self.script) class Shell(BaseModel): action_type : Literal["shell"] class AShell(BaseModel): action_type : Literal["shell"] = "shell" script : str def __call__(self, c : Container) -> Container: return c.run_shell(self.script) class Upload(BaseModel): action_type : Literal["upload"] class AUpload(BaseModel): action_type : Literal["upload"] = "upload" src : Path # local dst : Path # remote extract : bool = False # use ADD instead of COPY Loading @@ -41,8 +42,8 @@ class Upload(BaseModel): cmd = f"ADD {self.src} {self.dst}" return c.run_cmd(cmd) class Download(BaseModel): action_type : Literal["download"] class ADownload(BaseModel): action_type : Literal["download"] = "download" src : Path # remote dst : Path # local Loading @@ -51,17 +52,49 @@ class Download(BaseModel): f"{c.name}:{self.src}", self.dst) return c Actions = Union[Return, Script, Shell, Upload, Download] class Action(BaseModel): action: Actions = Field(..., discriminator="action_type") ActionT = Annotated[ Union[AReturn, AScript, AShell, AUpload, ADownload], Field(discriminator="action_type")] class Action: def __init__(self, run : Callable[[Container], Container]): self.action = run def __call__(self, c : Container) -> Container: return self.action(c) def apply(c : Container, action : Action) -> Container: return action(c) def apply(self, *acts : "Action") -> "Action": def run(c : Container) -> Container: c = self(c) for act in acts: c = act(c) return c return Action(run) def applyM(self, k : Callable[[str], "Action"] ) -> "Action": def run(c : Container) -> Container: c = self(c) # first run this action A = k(c.value) # extract the next action return A( c ) return Action(run) def Return(value : str) -> Action: return Action(AReturn(value=value)) def Script(script : str) -> Action: return Action(AScript(script=script)) def Shell(script : str) -> Action: return Action(AShell(script = script)) def Upload(src : Union[str,Path], dst : Union[str,Path], extract : bool = False) -> Action: return Action(AUpload(src=Path(src), dst=Path(dst), extract=extract)) def applyM(c : Container, k : Callable[[str], Action]) -> Container: action = k(c.value) return action(c) def Download(src : Union[str,Path], dst : Union[str,Path] ) -> Action: return Action(ADownload(src=Path(src), dst=Path(dst))) contaminate/container.py +25 −0 Original line number Diff line number Diff line from typing import Optional, Union from pathlib import Path import json from pydantic import BaseModel Loading @@ -6,6 +8,9 @@ from .container_env import ContainerEnv import contaminate.podman as podman def incr_name(name : str) -> str: s = name.split(":", 1) if len(s) > 1: name = s[0] s = name.rsplit(".", 1) if len(s) == 1: return f"{name}.1" Loading @@ -21,6 +26,26 @@ class Container(BaseModel): env : ContainerEnv # run-environment needed to use container value : str # current container output text @classmethod def read(cls, fname : Union[str,Path]) -> "Container": with open(fname, "r", encoding="utf-8") as f: return Container.model_validate_json(f.read()) def write(self, fname : Union[str,Path]) -> None: Path(fname).write_text( self.model_dump_json(indent=4)) def run(self, *actions, name: Optional[str] = None ) -> "Container": assert len(actions) > 0, "inaction not implemented" c = self.copy() end = len(actions)-1 for i, a in enumerate(actions): if name is not None and i == end: # rename on last step c.name = name c = a(c) return c def with_value(self, val : str) -> "Container": return Container(name=self.name, #path=self.path, env=self.env, value=val) Loading Loading
README.md +18 −7 Original line number Diff line number Diff line Loading @@ -31,20 +31,31 @@ To instantiate it, create a container environment, then a base container with that environment. Then apply the action. from contaminate import Container, ContainerEnv, apply from contaminate import Container, ContainerEnv env = ContainerEnv(system="localhost", arch="linux/x86_64", bind=["/home"]) c = Container(name="ubuntu:16.04", env=env, value="") c2 = apply(c, build_essentials) c2 = c.run(build_essentials) print(c2.name) print(c2.name) # ubuntu.1 You can also run commands directly in the container You can also chain/compose commands with `apply`, run commands directly in the container using the Shell action, and copy files from the container using the Download action, apply(c2, Shell(script="w")) apply(c2, Download(src="/etc/hosts", dst="hostfile")) steps = Shell(script="w") \ .apply(Download(src="/etc/hosts", dst="hostfile")) c2.run(steps) The coontainer build process makes use of `podman build`, so the images are stored based on podman's conventions. You can save and load the extended container environment information using: c2.write("my_container.json") c2 = Container.read("my_container.json")
contaminate/__init__.py +1 −1 Original line number Diff line number Diff line from .container import Container from .container_env import ContainerEnv from .action import Return, Script, Shell, Upload, Download, applyM from .action import Return, Script, Shell, Upload, Download
contaminate/action.py +53 −20 Original line number Diff line number Diff line from enum import Enum from typing import Literal, Union, Callable from typing_extensions import Annotated from pathlib import Path from pydantic import BaseModel, Field Loading @@ -8,29 +9,29 @@ from .container import Container from .container_env import ContainerEnv from .podman import runcmd class Return(BaseModel): action_type : Literal["return"] class AReturn(BaseModel): action_type : Literal["return"] = "return" value : str def __call__(self, c : Container) -> Container: return c.with_value(self.value) class Script(BaseModel): action_type : Literal["script"] class AScript(BaseModel): action_type : Literal["script"] = "script" script : str def __call__(self, c : Container) -> Container: return c.run_cmd(self.script) class Shell(BaseModel): action_type : Literal["shell"] class AShell(BaseModel): action_type : Literal["shell"] = "shell" script : str def __call__(self, c : Container) -> Container: return c.run_shell(self.script) class Upload(BaseModel): action_type : Literal["upload"] class AUpload(BaseModel): action_type : Literal["upload"] = "upload" src : Path # local dst : Path # remote extract : bool = False # use ADD instead of COPY Loading @@ -41,8 +42,8 @@ class Upload(BaseModel): cmd = f"ADD {self.src} {self.dst}" return c.run_cmd(cmd) class Download(BaseModel): action_type : Literal["download"] class ADownload(BaseModel): action_type : Literal["download"] = "download" src : Path # remote dst : Path # local Loading @@ -51,17 +52,49 @@ class Download(BaseModel): f"{c.name}:{self.src}", self.dst) return c Actions = Union[Return, Script, Shell, Upload, Download] class Action(BaseModel): action: Actions = Field(..., discriminator="action_type") ActionT = Annotated[ Union[AReturn, AScript, AShell, AUpload, ADownload], Field(discriminator="action_type")] class Action: def __init__(self, run : Callable[[Container], Container]): self.action = run def __call__(self, c : Container) -> Container: return self.action(c) def apply(c : Container, action : Action) -> Container: return action(c) def apply(self, *acts : "Action") -> "Action": def run(c : Container) -> Container: c = self(c) for act in acts: c = act(c) return c return Action(run) def applyM(self, k : Callable[[str], "Action"] ) -> "Action": def run(c : Container) -> Container: c = self(c) # first run this action A = k(c.value) # extract the next action return A( c ) return Action(run) def Return(value : str) -> Action: return Action(AReturn(value=value)) def Script(script : str) -> Action: return Action(AScript(script=script)) def Shell(script : str) -> Action: return Action(AShell(script = script)) def Upload(src : Union[str,Path], dst : Union[str,Path], extract : bool = False) -> Action: return Action(AUpload(src=Path(src), dst=Path(dst), extract=extract)) def applyM(c : Container, k : Callable[[str], Action]) -> Container: action = k(c.value) return action(c) def Download(src : Union[str,Path], dst : Union[str,Path] ) -> Action: return Action(ADownload(src=Path(src), dst=Path(dst)))
contaminate/container.py +25 −0 Original line number Diff line number Diff line from typing import Optional, Union from pathlib import Path import json from pydantic import BaseModel Loading @@ -6,6 +8,9 @@ from .container_env import ContainerEnv import contaminate.podman as podman def incr_name(name : str) -> str: s = name.split(":", 1) if len(s) > 1: name = s[0] s = name.rsplit(".", 1) if len(s) == 1: return f"{name}.1" Loading @@ -21,6 +26,26 @@ class Container(BaseModel): env : ContainerEnv # run-environment needed to use container value : str # current container output text @classmethod def read(cls, fname : Union[str,Path]) -> "Container": with open(fname, "r", encoding="utf-8") as f: return Container.model_validate_json(f.read()) def write(self, fname : Union[str,Path]) -> None: Path(fname).write_text( self.model_dump_json(indent=4)) def run(self, *actions, name: Optional[str] = None ) -> "Container": assert len(actions) > 0, "inaction not implemented" c = self.copy() end = len(actions)-1 for i, a in enumerate(actions): if name is not None and i == end: # rename on last step c.name = name c = a(c) return c def with_value(self, val : str) -> "Container": return Container(name=self.name, #path=self.path, env=self.env, value=val) Loading