Skip to content

Commit 51db435

Browse files
authored
Restore the behaviour of --upload-fragment for acifragmentgen (#9477)
Addresses - #9222 - [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. <!--- Please provide the related command with az {command} if you can, so that we can quickly route to the related person to review. ---> - [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 51db435

17 files changed

+907
-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 confcom fragment push command to allow explicit uploading of standalone fragments
10+
* added confcom fragment attach command 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: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55

66
import os
77
import sys
8-
from typing import Optional
8+
import tempfile
9+
from typing import Optional, BinaryIO
910

1011
from azext_confcom import oras_proxy, os_util, security_policy
1112
from azext_confcom._validators import resolve_stdio
@@ -22,6 +23,8 @@
2223
get_image_name, inject_policy_into_template, inject_policy_into_yaml,
2324
pretty_print_func, print_existing_policy_from_arm_template,
2425
print_existing_policy_from_yaml, print_func, str_to_sha256)
26+
from azext_confcom.command.fragment_attach import fragment_attach as _fragment_attach
27+
from azext_confcom.command.fragment_push import fragment_push as _fragment_push
2528
from knack.log import get_logger
2629
from pkg_resources import parse_version
2730

@@ -255,6 +258,7 @@ def acifragmentgen_confcom(
255258
upload_fragment: bool = False,
256259
no_print: bool = False,
257260
fragments_json: str = "",
261+
out_signed_fragment: bool = False,
258262
):
259263
if container_definitions is None:
260264
container_definitions = []
@@ -361,24 +365,40 @@ def acifragmentgen_confcom(
361365

362366
fragment_text = policy.generate_fragment(namespace, svn, output_type, omit_id=omit_id)
363367

364-
if output_type != security_policy.OutputType.DEFAULT and not no_print:
368+
if output_type != security_policy.OutputType.DEFAULT and not no_print and not out_signed_fragment:
365369
print(fragment_text)
366370

367371
# take ".rego" off the end of the filename if it's there, it'll get added back later
368372
output_filename = output_filename.replace(".rego", "")
369373
filename = f"{output_filename or namespace}.rego"
374+
375+
if out_signed_fragment:
376+
filename = os.path.join(tempfile.gettempdir(), filename)
377+
370378
os_util.write_str_to_file(filename, fragment_text)
371379

372380
if key:
373381
cose_proxy = CoseSignToolProxy()
374382
iss = cose_proxy.create_issuer(chain)
375383
out_path = filename + ".cose"
376384

385+
if out_signed_fragment:
386+
out_path = os.path.join(tempfile.gettempdir(), os.path.basename(out_path))
387+
377388
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)
389+
390+
# Preserve default behaviour established since version 1.1.0 of attaching
391+
# the fragment to the first image specified in input
392+
# (or --image-target if specified)
393+
if upload_fragment:
394+
oras_proxy.attach_fragment_to_image(
395+
image_name=image_target or policy_images[0].containerImage,
396+
filename=out_path,
397+
)
398+
399+
if out_signed_fragment:
400+
with open(out_path, "rb") as f:
401+
sys.stdout.buffer.write(f.read())
382402

383403

384404
def katapolicygen_confcom(
@@ -512,3 +532,23 @@ def get_fragment_output_type(outraw):
512532
if outraw:
513533
output_type = security_policy.OutputType.RAW
514534
return output_type
535+
536+
537+
def fragment_attach(
538+
signed_fragment: BinaryIO,
539+
manifest_tag: str,
540+
) -> None:
541+
_fragment_attach(
542+
signed_fragment=signed_fragment,
543+
manifest_tag=manifest_tag
544+
)
545+
546+
547+
def fragment_push(
548+
signed_fragment: BinaryIO,
549+
manifest_tag: str,
550+
) -> None:
551+
_fragment_push(
552+
signed_fragment=signed_fragment,
553+
manifest_tag=manifest_tag
554+
)

0 commit comments

Comments
 (0)