Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions linter_exclusions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3525,3 +3525,9 @@ confcom fragment attach:
signed_fragment:
rule_exclusions:
- no_positional_parameters

confcom containers from_image:
parameters:
image:
rule_exclusions:
- no_positional_parameters
25 changes: 25 additions & 0 deletions src/confcom/azext_confcom/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -321,3 +321,28 @@
- name: Attach the output of acifragmentgen to a registry
text: az confcom acifragmentgen --chain my.cert.pem --key my_key.pem --svn "1" --namespace contoso --feed "test-feed" --input ./fragment_spec.json | az confcom fragment attach --manifest-tag myregistry.azurecr.io/image:latest
"""

helps[
"confcom containers"
] = """
type: group
short-summary: Commands which generate Security Policy Container Definitions.
"""


helps[
"confcom containers from_image"
] = """
type: command
short-summary: Create a Security Policy Container Definition based on a Radius app template.

parameters:
- name: --platform
type: str
short-summary: 'The name of the platform the container definition will run on'


examples:
- name: Input an image reference and generate container definitions
text: az confcom containers from_image my.azurecr.io/myimage:tag
"""
15 changes: 15 additions & 0 deletions src/confcom/azext_confcom/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -469,3 +469,18 @@ def load_arguments(self, _):
help="Path to containerd socket if not using the default",
validator=validate_katapolicygen_input,
)

with self.argument_context("confcom containers from_image") as c:
c.positional(
"image",
type=str,
help="Image to create container definition from",
)
c.argument(
"platform",
options_list=("--platform",),
required=False,
default="aci",
type=str,
help="Platform to create container definition for",
)
12 changes: 12 additions & 0 deletions src/confcom/azext_confcom/command/containers_from_image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

import json

from azext_confcom.lib.containers import from_image as lib_containers_from_image


def containers_from_image(image: str, platform: str) -> str:
return print(json.dumps(lib_containers_from_image(image, platform)))
3 changes: 3 additions & 0 deletions src/confcom/azext_confcom/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,6 @@ def load_command_table(self, _):

with self.command_group("confcom"):
pass

with self.command_group("confcom containers") as g:
g.custom_command("from_image", "containers_from_image")
11 changes: 11 additions & 0 deletions src/confcom/azext_confcom/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
print_existing_policy_from_yaml, print_func, str_to_sha256)
from azext_confcom.command.fragment_attach import fragment_attach as _fragment_attach
from azext_confcom.command.fragment_push import fragment_push as _fragment_push
from azext_confcom.command.containers_from_image import containers_from_image as _containers_from_image
from knack.log import get_logger
from pkg_resources import parse_version

Expand Down Expand Up @@ -552,3 +553,13 @@ def fragment_push(
signed_fragment=signed_fragment,
manifest_tag=manifest_tag
)


def containers_from_image(
image: str,
platform: str,
) -> None:
_containers_from_image(
image=image,
platform=platform,
)
23 changes: 23 additions & 0 deletions src/confcom/azext_confcom/lib/containers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

from dataclasses import asdict
from azext_confcom.lib.images import get_image_layers, get_image_config
from azext_confcom.lib.platform import ACI_MOUNTS


def from_image(image: str, platform: str) -> str:

mounts = {
"aci": [asdict(mount) for mount in ACI_MOUNTS],
}.get(platform, None)

return {
"id": image,
"name": image,
"layers": get_image_layers(image),
**({"mounts": mounts} if mounts else {}),
**get_image_config(image),
}
65 changes: 65 additions & 0 deletions src/confcom/azext_confcom/lib/images.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

import functools
import subprocess
import docker

from pathlib import Path


@functools.lru_cache()
def get_image(image_ref: str) -> docker.models.images.Image:

client = docker.from_env()

try:
image = client.images.get(image_ref)
except docker.errors.ImageNotFound:
client.images.pull(image_ref)

image = client.images.get(image_ref)
return image


def get_image_layers(image: str) -> list[str]:

binary_path = Path(__file__).parent.parent / "bin" / "dmverity-vhd"

get_image(image)
result = subprocess.run(
[binary_path.as_posix(), "-d", "roothash", "-i", image],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
check=True,
text=True,
)

return [line.split("hash: ")[-1] for line in result.stdout.splitlines()]


def get_image_config(image: str) -> dict:

image_config = get_image(image).attrs.get("Config")

config = {}

if image_config.get("Cmd") or image_config.get("Entrypoint"):
config["command"] = (
image_config.get("Entrypoint") or [] +
image_config.get("Cmd") or []
)

if image_config.get("Env"):
config["env_rules"] = [{
"pattern": p,
"strategy": "string",
"required": False,
} for p in image_config.get("Env")]

if image_config.get("WorkingDir"):
config["working_dir"] = image_config.get("WorkingDir")

return config
19 changes: 19 additions & 0 deletions src/confcom/azext_confcom/lib/platform.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

from azext_confcom.lib.policy import ContainerMount

ACI_MOUNTS = [
ContainerMount(
destination="/etc/resolv.conf",
options=[
"rbind",
"rshared",
"rw"
],
source="sandbox:///tmp/atlas/resolvconf/.+",
type="bind"
)
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

import json
import tempfile
import docker
import pytest
import portalocker

from contextlib import redirect_stdout
from io import StringIO
from itertools import product
from pathlib import Path
from deepdiff import DeepDiff

from azext_confcom.command.containers_from_image import containers_from_image


TEST_DIR = Path(__file__).parent
CONFCOM_DIR = TEST_DIR.parent.parent.parent
SAMPLES_ROOT = CONFCOM_DIR / "samples" / "images"
DOCKER_LOCK = Path(tempfile.gettempdir()) / "confcom-docker.lock"


@pytest.fixture(scope="session", autouse=True)
def build_test_containers():

docker_client = docker.from_env()
with portalocker.Lock(DOCKER_LOCK.as_posix(), timeout=20):
for image_sample in SAMPLES_ROOT.iterdir():
docker_client.images.build(
path=str(image_sample),
tag=f"confcom_test_{image_sample.name}",
quiet=True,
rm=True,
)

yield


@pytest.mark.parametrize(
"sample_directory, platform",
product(
[p.name for p in SAMPLES_ROOT.iterdir()],
["aci"],
)
)
def test_containers_from_image(sample_directory: str, platform: str):

sample_directory = Path(SAMPLES_ROOT) / sample_directory

expected_container_def_path = sample_directory / f"{platform}_container.inc.rego"
with expected_container_def_path.open("r", encoding="utf-8") as handle:
expected_container_def = json.load(handle)

buffer = StringIO()
with redirect_stdout(buffer):
containers_from_image(
image=f"confcom_test_{sample_directory.name}",
platform=platform,
)

actual_container_def = json.loads(buffer.getvalue())

diff = DeepDiff(
actual_container_def,
expected_container_def,
ignore_order=True,
)
assert diff == {}, diff
3 changes: 3 additions & 0 deletions src/confcom/samples/images/command/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
FROM hello-world

CMD ["/command"]
30 changes: 30 additions & 0 deletions src/confcom/samples/images/command/aci_container.inc.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"id": "confcom_test_command",
"name": "confcom_test_command",
"layers": [
"8b4664979ffe3c5188efbbbb30e31716c03bfe880f15f455be0fc3beb4741de9"
],
"mounts": [
{
"destination": "/etc/resolv.conf",
"options": [
"rbind",
"rshared",
"rw"
],
"source": "sandbox:///tmp/atlas/resolvconf/.+",
"type": "bind"
}
],
"command": [
"/command"
],
"env_rules": [
{
"pattern": "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"strategy": "string",
"required": false
}
],
"working_dir": "/"
}
3 changes: 3 additions & 0 deletions src/confcom/samples/images/environment_variables/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
FROM hello-world

ENV TEST_ENV_VAR="Test Env Value"
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"id": "confcom_test_environment_variables",
"name": "confcom_test_environment_variables",
"layers": [
"8b4664979ffe3c5188efbbbb30e31716c03bfe880f15f455be0fc3beb4741de9"
],
"mounts": [
{
"destination": "/etc/resolv.conf",
"options": [
"rbind",
"rshared",
"rw"
],
"source": "sandbox:///tmp/atlas/resolvconf/.+",
"type": "bind"
}
],
"command": [
"/hello"
],
"env_rules": [
{
"pattern": "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"strategy": "string",
"required": false
},
{
"pattern": "TEST_ENV_VAR=Test Env Value",
"strategy": "string",
"required": false
}
],
"working_dir": "/"
}
1 change: 1 addition & 0 deletions src/confcom/samples/images/minimal/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
FROM hello-world
30 changes: 30 additions & 0 deletions src/confcom/samples/images/minimal/aci_container.inc.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"id": "confcom_test_minimal",
"name": "confcom_test_minimal",
"layers": [
"8b4664979ffe3c5188efbbbb30e31716c03bfe880f15f455be0fc3beb4741de9"
],
"mounts": [
{
"destination": "/etc/resolv.conf",
"options": [
"rbind",
"rshared",
"rw"
],
"source": "sandbox:///tmp/atlas/resolvconf/.+",
"type": "bind"
}
],
"command": [
"/hello"
],
"env_rules": [
{
"pattern": "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"strategy": "string",
"required": false
}
],
"working_dir": "/"
}
Loading
Loading