Skip to content

Commit ab1c94e

Browse files
authored
Restore the behaviour of --upload-fragment for acifragmentgen (#13)
### Why Addresses - #9222 ### How - [x] Update the code to restore the "attach to first image in input" behaviour - [x] Add two new commands: `fragment push` and `fragment attach` to allow the user to explicitly do one or the other (or both!) - [x] Add new tests which run a local docker registry, and test that the fragments are generated, signed, pushed and attached as expected (as well as the default behaviour) --- This checklist is used to make sure that common guidelines for a pull request are followed. ### Related command <!--- Please provide the related command with az {command} if you can, so that we can quickly route to the related person to review. ---> ### General Guidelines - [x] Have you run `azdev style <YOUR_EXT>` locally? (`pip install azdev` required) - [x] Have you run `python scripts/ci/test_index.py -q` locally? (`pip install wheel==0.30.0` required) - [x] My extension version conforms to the [Extension version schema](https://github.com/Azure/azure-cli/blob/release/doc/extensions/versioning_guidelines.md)
1 parent 1e7a157 commit ab1c94e

17 files changed

+904
-55
lines changed

linter_exclusions.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3504,3 +3504,15 @@ neon postgres organization:
35043504
neon postgres project:
35053505
rule_exclusions:
35063506
- require_wait_command_if_no_wait
3507+
3508+
confcom fragment push:
3509+
parameters:
3510+
signed_fragment:
3511+
rule_exclusions:
3512+
- no_positional_parameters
3513+
3514+
confcom fragment attach:
3515+
parameters:
3516+
signed_fragment:
3517+
rule_exclusions:
3518+
- no_positional_parameters

src/confcom/HISTORY.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@
33
Release History
44
===============
55

6+
1.5.0
7+
++++++
8+
* restored the behaviour of --upload-fragment in acifragmentgen to attach to first image in input
9+
* added --push-fragment-to flag to acifragmentgen to allow explicit uploading of standalone fragments
10+
* added --attach-fragment-to flag to acifragmentgen to allow explicit uploading of image attached fragments
11+
612
1.4.5
713
++++++
814
* Drop the dependency on OPA

src/confcom/azext_confcom/_help.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,3 +278,46 @@
278278
- name: Input a Kubernetes YAML file with a custom containerd socket path
279279
text: az confcom katapolicygen --yaml "./pod.json" --containerd-pull --containerd-socket-path "/my/custom/containerd.sock"
280280
"""
281+
282+
helps[
283+
"confcom fragment"
284+
] = """
285+
type: group
286+
short-summary: Commands to handle Confidential Container Policy Fragments.
287+
"""
288+
289+
helps[
290+
"confcom fragment push"
291+
] = """
292+
type: command
293+
short-summary: Push a Confidential Container Policy Fragment to an ORAS registry
294+
295+
parameters:
296+
- name: --manifest-tag
297+
type: string
298+
short-summary: 'The reference to push the signed fragment to'
299+
300+
examples:
301+
- name: Push a signed fragment to a registry
302+
text: az confcom fragment push ./fragment.reg.cose --manifest-tag myregistry.azurecr.io/fragment:latest
303+
- name: Push the output of acifragmentgen to a registry
304+
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 push --manifest-tag myregistry.azurecr.io/fragment:latest
305+
"""
306+
307+
helps[
308+
"confcom fragment attach"
309+
] = """
310+
type: command
311+
short-summary: Attach a Confidential Container Policy Fragment to an image in an ORAS registry.
312+
313+
parameters:
314+
- name: --manifest-tag
315+
type: string
316+
short-summary: 'The reference to attach the signed fragment to'
317+
318+
examples:
319+
- name: Attach a signed fragment to a registry
320+
text: az confcom fragment attach ./fragment.reg.cose --manifest-tag myregistry.azurecr.io/image:latest
321+
- name: Attach the output of acifragmentgen to a registry
322+
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
323+
"""

src/confcom/azext_confcom/_params.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
# pylint: disable=line-too-long
66

77
import json
8+
import argparse
9+
import sys
810
from knack.arguments import CLIArgumentType
911
from azext_confcom._validators import (
1012
validate_params_file,
@@ -44,6 +46,32 @@ def load_arguments(self, _):
4446
c.argument("tags", tags_type)
4547
c.argument("confcom_name", confcom_name_type, options_list=["--name", "-n"])
4648

49+
with self.argument_context("confcom fragment attach") as c:
50+
c.positional(
51+
"signed_fragment",
52+
nargs='?',
53+
type=argparse.FileType('rb'),
54+
default=sys.stdin.buffer,
55+
help="Signed fragment to attach",
56+
)
57+
c.argument(
58+
"manifest_tag",
59+
help="Manifest tag for the fragment",
60+
)
61+
62+
with self.argument_context("confcom fragment push") as c:
63+
c.positional(
64+
"signed_fragment",
65+
nargs='?',
66+
type=argparse.FileType('rb'),
67+
default=sys.stdin.buffer,
68+
help="Signed fragment to push",
69+
)
70+
c.argument(
71+
"manifest_tag",
72+
help="Manifest tag for the fragment",
73+
)
74+
4775
with self.argument_context("confcom acipolicygen") as c:
4876
c.argument(
4977
"input_path",
@@ -362,6 +390,13 @@ def load_arguments(self, _):
362390
type=json.loads,
363391
help='Container definitions to include in the policy'
364392
)
393+
c.argument(
394+
"out_signed_fragment",
395+
action="store_true",
396+
default=False,
397+
required=False,
398+
help="Emit only the signed fragment bytes",
399+
)
365400

366401
with self.argument_context("confcom katapolicygen") as c:
367402
c.argument(
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# --------------------------------------------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# Licensed under the MIT License. See License.txt in the project root for license information.
4+
# --------------------------------------------------------------------------------------------
5+
6+
import os
7+
import subprocess
8+
import tempfile
9+
from typing import BinaryIO
10+
11+
12+
def oras_attach(
13+
signed_fragment: BinaryIO,
14+
manifest_tag: str,
15+
) -> None:
16+
subprocess.run(
17+
[
18+
"oras",
19+
"attach",
20+
"--artifact-type", "application/x-ms-ccepolicy-frag",
21+
manifest_tag,
22+
os.path.relpath(signed_fragment.name, start=os.getcwd()),
23+
],
24+
check=True,
25+
timeout=120,
26+
)
27+
28+
29+
def fragment_attach(
30+
signed_fragment: BinaryIO,
31+
manifest_tag: str,
32+
) -> None:
33+
34+
if signed_fragment.name == "<stdin>":
35+
with tempfile.NamedTemporaryFile(delete=True) as temp_signed_fragment:
36+
temp_signed_fragment.write(signed_fragment.read())
37+
temp_signed_fragment.flush()
38+
oras_attach(
39+
signed_fragment=temp_signed_fragment,
40+
manifest_tag=manifest_tag,
41+
)
42+
else:
43+
oras_attach(
44+
signed_fragment=signed_fragment,
45+
manifest_tag=manifest_tag,
46+
)
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# --------------------------------------------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# Licensed under the MIT License. See License.txt in the project root for license information.
4+
# --------------------------------------------------------------------------------------------
5+
6+
import os
7+
import subprocess
8+
import tempfile
9+
from typing import BinaryIO
10+
11+
12+
def oras_push(
13+
signed_fragment: BinaryIO,
14+
manifest_tag: str,
15+
) -> None:
16+
subprocess.run(
17+
[
18+
"oras",
19+
"push",
20+
"--artifact-type", "application/x-ms-ccepolicy-frag",
21+
manifest_tag,
22+
os.path.relpath(signed_fragment.name, start=os.getcwd()),
23+
],
24+
check=True,
25+
timeout=120,
26+
)
27+
28+
29+
def fragment_push(
30+
signed_fragment: BinaryIO,
31+
manifest_tag: str,
32+
) -> None:
33+
34+
if signed_fragment.name == "<stdin>":
35+
with tempfile.NamedTemporaryFile(delete=True) as temp_signed_fragment:
36+
temp_signed_fragment.write(signed_fragment.read())
37+
temp_signed_fragment.flush()
38+
oras_push(
39+
signed_fragment=temp_signed_fragment,
40+
manifest_tag=manifest_tag,
41+
)
42+
else:
43+
oras_push(
44+
signed_fragment=signed_fragment,
45+
manifest_tag=manifest_tag,
46+
)

src/confcom/azext_confcom/commands.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,9 @@ def load_command_table(self, _):
1111
g.custom_command("acifragmentgen", "acifragmentgen_confcom")
1212
g.custom_command("katapolicygen", "katapolicygen_confcom")
1313

14+
with self.command_group("confcom fragment") as g:
15+
g.custom_command("attach", "fragment_attach", is_preview=True)
16+
g.custom_command("push", "fragment_push", is_preview=True)
17+
1418
with self.command_group("confcom"):
1519
pass

src/confcom/azext_confcom/custom.py

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import os
77
import sys
8-
from typing import Optional
8+
from typing import Optional, BinaryIO
99

1010
from azext_confcom import oras_proxy, os_util, security_policy
1111
from azext_confcom._validators import resolve_stdio
@@ -22,6 +22,8 @@
2222
get_image_name, inject_policy_into_template, inject_policy_into_yaml,
2323
pretty_print_func, print_existing_policy_from_arm_template,
2424
print_existing_policy_from_yaml, print_func, str_to_sha256)
25+
from azext_confcom.command.fragment_attach import fragment_attach as _fragment_attach
26+
from azext_confcom.command.fragment_push import fragment_push as _fragment_push
2527
from knack.log import get_logger
2628
from pkg_resources import parse_version
2729

@@ -255,6 +257,7 @@ def acifragmentgen_confcom(
255257
upload_fragment: bool = False,
256258
no_print: bool = False,
257259
fragments_json: str = "",
260+
out_signed_fragment: bool = False,
258261
):
259262
if container_definitions is None:
260263
container_definitions = []
@@ -361,24 +364,39 @@ def acifragmentgen_confcom(
361364

362365
fragment_text = policy.generate_fragment(namespace, svn, output_type, omit_id=omit_id)
363366

364-
if output_type != security_policy.OutputType.DEFAULT and not no_print:
367+
if output_type != security_policy.OutputType.DEFAULT and not no_print and not out_signed_fragment:
365368
print(fragment_text)
366369

367370
# take ".rego" off the end of the filename if it's there, it'll get added back later
368371
output_filename = output_filename.replace(".rego", "")
369372
filename = f"{output_filename or namespace}.rego"
373+
374+
if out_signed_fragment:
375+
filename = os.path.join("/tmp", filename)
376+
370377
os_util.write_str_to_file(filename, fragment_text)
371378

372379
if key:
373380
cose_proxy = CoseSignToolProxy()
374381
iss = cose_proxy.create_issuer(chain)
375382
out_path = filename + ".cose"
376383

384+
if out_signed_fragment:
385+
out_path = os.path.join("/tmp", os.path.basename(out_path))
386+
377387
cose_proxy.cose_sign(filename, key, chain, feed, iss, algo, out_path)
378-
if upload_fragment and image_target:
379-
oras_proxy.attach_fragment_to_image(image_target, out_path)
380-
elif upload_fragment:
381-
oras_proxy.push_fragment_to_registry(feed, out_path)
388+
389+
# Preserve default behaviour established since version 1.1.0 of attaching
390+
# the fragment to the first image specified in input
391+
# (or --image-target if specified)
392+
if upload_fragment:
393+
oras_proxy.attach_fragment_to_image(
394+
image_name=image_target or policy_images[0].containerImage,
395+
filename=out_path,
396+
)
397+
398+
if out_signed_fragment:
399+
sys.stdout.buffer.write(open(out_path, "rb").read())
382400

383401

384402
def katapolicygen_confcom(
@@ -512,3 +530,23 @@ def get_fragment_output_type(outraw):
512530
if outraw:
513531
output_type = security_policy.OutputType.RAW
514532
return output_type
533+
534+
535+
def fragment_attach(
536+
signed_fragment: BinaryIO,
537+
manifest_tag: str,
538+
) -> None:
539+
_fragment_attach(
540+
signed_fragment=signed_fragment,
541+
manifest_tag=manifest_tag
542+
)
543+
544+
545+
def fragment_push(
546+
signed_fragment: BinaryIO,
547+
manifest_tag: str,
548+
) -> None:
549+
_fragment_push(
550+
signed_fragment=signed_fragment,
551+
manifest_tag=manifest_tag
552+
)

0 commit comments

Comments
 (0)