Skip to content

Commit 7b8a17a

Browse files
himani2411Himani Deshpande
andauthored
[3.x] Add Integration tests for Resource Prefix (#4652) (#4735)
* Add Integration tests for Resource Prefix Add Integration test cases and config file for Iam Resource Prefix in test_iam.py Add user-role-rp.cfn.yaml to provide a User Role for creating test clusters Add the test case in pcluster3.yaml config for Jenkins tests. * Remove File deleteS3.py * Change according to PR Update the pcluster.config.yaml file to remove Iam section and use _inject_resource_in_config() to inject Iam and ResourcePrefix Section Change scope of initialize and register_prefix_cli_credentials from class to default(function) level Update _test_iam_resource_in_cluster() to add Cluster creation verification Update test_iam_resource_prefix to remove duplication of user-role-rp for each value of iam_resource_prefix_list test and improve performance. Change position of user-role-iam-resource-prefix.cfn.yaml to the tests folder directory Remove update_config variable from test_iam_resource_prefix PR Link: #4652 * Change Iam Resource Prefix tests to handle one test case Add Iam/ResourcePrefix in pcluster.config.yaml Change user-role-iam-resource-prefix.cfn.yaml to handle /path-prefix/name-prefix- Iam Resource Prefix Add /path-prefix/name-prefix as the only pytest parameter and remove use_default_iam_credentials parameter check condition from initialize_resource_prefix_cli_creds * Add Test specific CLI credentials in cluster creation Add test specific CLI credentials while creation of cluster and run pcluster commands. Change ClusterFactory's create_cluster() to have test specific CLI credentials as arguments Change user-role-iam-resource-prefix.cfn.yaml to handle /path-prefix/name-prefix- Iam Resource Prefix Remove register_resource_prefix_cli_credentials as it will affect parallel tests running in same region. * Remove unnecessary methods and keyword Arguments Remove _inject_resource_in_config() which is unused for this PR( part of another PR) Revert run_command() to its original definition Update run_pcluster_command() to handle switching and KeyError for credential_arn Remove unnecessary key word arguments passed from create_cluster() to run_pcluster_command() Reference PR: #4652 * Remove custom_cli_credentials from ClusterFactory Removing custom_cli_credentials from Cluster Factory class member. Reference PR: #4652 * Revert change in run_pcluster_command Revert the changes in indentation and scope of if clause in run_pcluster_command() Reference PR: #4652 Co-authored-by: Himani Deshpande <[email protected]> Co-authored-by: Himani Deshpande <[email protected]>
1 parent 3942dc5 commit 7b8a17a

File tree

9 files changed

+1014
-21
lines changed

9 files changed

+1014
-21
lines changed

tests/integration-tests/clusters_factory.py

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def wrapper(*args, **kwargs):
4444
class Cluster:
4545
"""Contain all static and dynamic data related to a cluster instance."""
4646

47-
def __init__(self, name, ssh_key, config_file, region):
47+
def __init__(self, name, ssh_key, config_file, region, custom_cli_credentials=None):
4848
self.name = name
4949
self.config_file = config_file
5050
self.ssh_key = ssh_key
@@ -57,6 +57,7 @@ def __init__(self, name, ssh_key, config_file, region):
5757
self.__cfn_outputs = None
5858
self.__cfn_resources = None
5959
self.__cfn_stack_arn = None
60+
self.custom_cli_credentials = custom_cli_credentials
6061

6162
def __repr__(self):
6263
attrs = ", ".join(["{key}={value}".format(key=key, value=repr(value)) for key, value in self.__dict__.items()])
@@ -89,7 +90,12 @@ def update(self, config_file, raise_on_error=True, log_error=True, **kwargs):
8990
# TODO Remove the validator suppression below once the plugin scheduler is officially supported
9091
if self.config["Scheduling"]["Scheduler"] == "plugin":
9192
command.extend(["--suppress-validators", "type:SchedulerValidator"])
92-
result = run_pcluster_command(command, raise_on_error=raise_on_error, log_error=log_error)
93+
result = run_pcluster_command(
94+
command,
95+
raise_on_error=raise_on_error,
96+
log_error=log_error,
97+
custom_cli_credentials=self.custom_cli_credentials,
98+
)
9399
logging.info("update-cluster response: %s", result.stdout)
94100
response = json.loads(result.stdout)
95101
if response.get("cloudFormationStackStatus") != "UPDATE_COMPLETE":
@@ -130,7 +136,7 @@ def delete(self, delete_logs=False):
130136
logging.warning("CloudWatch logs for cluster %s are preserved due to failure.", self.name)
131137
try:
132138
self.cfn_stack_arn # Cache cfn_stack_arn attribute before stack deletion
133-
result = run_pcluster_command(cmd_args, log_error=False)
139+
result = run_pcluster_command(cmd_args, log_error=False, custom_cli_credentials=self.custom_cli_credentials)
134140
if "DELETE_FAILED" in result.stdout:
135141
error = "Cluster deletion failed for {0} with output: {1}".format(self.name, result.stdout)
136142
logging.error(error)
@@ -153,7 +159,7 @@ def start(self):
153159
else: # slurm and scheduler plugin case
154160
cmd_args.append("START_REQUESTED")
155161
try:
156-
result = run_pcluster_command(cmd_args, log_error=False)
162+
result = run_pcluster_command(cmd_args, log_error=False, custom_cli_credentials=self.custom_cli_credentials)
157163
logging.info("Cluster {0} started successfully".format(self.name))
158164
return result.stdout
159165
except subprocess.CalledProcessError as e:
@@ -169,7 +175,7 @@ def stop(self):
169175
else: # slurm and scheduler plugin case
170176
cmd_args.append("STOP_REQUESTED")
171177
try:
172-
result = run_pcluster_command(cmd_args, log_error=False)
178+
result = run_pcluster_command(cmd_args, log_error=False, custom_cli_credentials=self.custom_cli_credentials)
173179
logging.info("Cluster {0} stopped successfully".format(self.name))
174180
return result.stdout
175181
except subprocess.CalledProcessError as e:
@@ -180,7 +186,7 @@ def describe_cluster(self):
180186
"""Run pcluster describe-cluster and return the result."""
181187
cmd_args = ["pcluster", "describe-cluster", "--cluster-name", self.name]
182188
try:
183-
result = run_pcluster_command(cmd_args, log_error=False)
189+
result = run_pcluster_command(cmd_args, log_error=False, custom_cli_credentials=self.custom_cli_credentials)
184190
response = json.loads(result.stdout)
185191
logging.info("Get cluster {0} status successfully".format(self.name))
186192
return response
@@ -192,7 +198,7 @@ def describe_compute_fleet(self):
192198
"""Run pcluster describe-compute-fleet and return the result."""
193199
cmd_args = ["pcluster", "describe-compute-fleet", "--cluster-name", self.name]
194200
try:
195-
result = run_pcluster_command(cmd_args, log_error=False)
201+
result = run_pcluster_command(cmd_args, log_error=False, custom_cli_credentials=self.custom_cli_credentials)
196202
response = json.loads(result.stdout)
197203
logging.info("Describe cluster %s compute fleet successfully", self.name)
198204
return response
@@ -216,7 +222,7 @@ def describe_cluster_instances(self, node_type=None, queue_name=None):
216222
if queue_name:
217223
cmd_args.extend(["--queue-name", queue_name])
218224
try:
219-
result = run_pcluster_command(cmd_args, log_error=False)
225+
result = run_pcluster_command(cmd_args, log_error=False, custom_cli_credentials=self.custom_cli_credentials)
220226
response = json.loads(result.stdout)
221227
logging.info("Get cluster {0} instances successfully".format(self.name))
222228
return response["instances"]
@@ -239,7 +245,7 @@ def export_logs(self, bucket, output_file=None, bucket_prefix=None, filters=None
239245
if filters:
240246
cmd_args += ["--filters", filters]
241247
try:
242-
result = run_pcluster_command(cmd_args, log_error=False)
248+
result = run_pcluster_command(cmd_args, log_error=False, custom_cli_credentials=self.custom_cli_credentials)
243249
response = json.loads(result.stdout)
244250
logging.info("Cluster's logs exported successfully")
245251
return response
@@ -253,7 +259,7 @@ def list_log_streams(self, next_token=None):
253259
if next_token:
254260
cmd_args.extend(["--next-token", next_token])
255261
try:
256-
result = run_pcluster_command(cmd_args, log_error=False)
262+
result = run_pcluster_command(cmd_args, log_error=False, custom_cli_credentials=self.custom_cli_credentials)
257263
response = json.loads(result.stdout)
258264
logging.info("Cluster's logs listed successfully")
259265
return response
@@ -281,7 +287,7 @@ def get_log_events(self, log_stream, **args):
281287
cmd_args.extend([f"--{kebab_case(k)}", str(val)])
282288

283289
try:
284-
result = run_pcluster_command(cmd_args, log_error=False)
290+
result = run_pcluster_command(cmd_args, log_error=False, custom_cli_credentials=self.custom_cli_credentials)
285291
response = json.loads(result.stdout)
286292
logging.info("Log events retrieved successfully")
287293
return response
@@ -296,7 +302,7 @@ def get_stack_events(self, **args):
296302
cmd_args.extend([f"--{kebab_case(k)}", str(val)])
297303

298304
try:
299-
result = run_pcluster_command(cmd_args, log_error=False)
305+
result = run_pcluster_command(cmd_args, log_error=False, custom_cli_credentials=self.custom_cli_credentials)
300306
response = json.loads(result.stdout)
301307
logging.info("Stack events retrieved successfully")
302308
return response
@@ -419,8 +425,13 @@ def create_cluster(self, cluster, log_error=True, raise_on_error=True, **kwargs)
419425
logging.info("Creating cluster {0} with config {1}".format(name, cluster.config_file))
420426
command, wait = self._build_command(cluster, kwargs)
421427
try:
422-
result = run_pcluster_command(command, timeout=7200, raise_on_error=raise_on_error, log_error=log_error)
423-
428+
result = run_pcluster_command(
429+
command,
430+
timeout=7200,
431+
raise_on_error=raise_on_error,
432+
log_error=log_error,
433+
custom_cli_credentials=kwargs.get("custom_cli_credentials"),
434+
)
424435
logging.info("create-cluster response: %s", result.stdout)
425436
response = json.loads(result.stdout)
426437
if wait:
@@ -470,10 +481,11 @@ def _build_command(cluster, kwargs):
470481
kwargs["suppress_validators"] = validators_list
471482

472483
for k, val in kwargs.items():
473-
if isinstance(val, (list, tuple)):
474-
command.extend([f"--{kebab_case(k)}"] + list(map(str, val)))
475-
else:
476-
command.extend([f"--{kebab_case(k)}", str(val)])
484+
if k != "custom_cli_credentials":
485+
if isinstance(val, (list, tuple)):
486+
command.extend([f"--{kebab_case(k)}"] + list(map(str, val)))
487+
else:
488+
command.extend([f"--{kebab_case(k)}", str(val)])
477489

478490
return command, wait
479491

tests/integration-tests/configs/common/common.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,12 @@ iam:
294294
instances: {{ common.INSTANCES_DEFAULT_X86 }}
295295
oss: ["alinux2"]
296296
schedulers: ["slurm", "awsbatch"]
297+
test_iam.py::test_iam_resource_prefix:
298+
dimensions:
299+
- regions: [ "eu-north-1" ]
300+
instances: {{ common.INSTANCES_DEFAULT_X86 }}
301+
oss: [ "alinux2" ]
302+
schedulers: [ "slurm" ]
297303
intel_hpc:
298304
test_intel_hpc.py::test_intel_hpc:
299305
dimensions:

tests/integration-tests/configs/pcluster3.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@ test-suites:
5050
instances: {{ common.INSTANCES_DEFAULT_X86 }}
5151
oss: ["alinux2"]
5252
schedulers: ["slurm"]
53+
test_iam.py::test_iam_resource_prefix:
54+
dimensions:
55+
- regions: [ "eu-north-1" ]
56+
schedulers: [ "slurm" ]
57+
oss: [ "alinux2" ]
58+
instances: {{ common.INSTANCES_DEFAULT_X86 }}
5359
schedulers:
5460
test_awsbatch.py::test_awsbatch:
5561
dimensions:

tests/integration-tests/conftest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,7 @@ def _cluster_factory(cluster_config, upper_case_cluster_name=False, **kwargs):
386386
config_file=cluster_config,
387387
ssh_key=request.config.getoption("key_path"),
388388
region=region,
389+
custom_cli_credentials=kwargs.get("custom_cli_credentials"),
389390
)
390391
if not request.config.getoption("cluster"):
391392
cluster.creation_response = factory.create_cluster(cluster, **kwargs)

tests/integration-tests/framework/credential_providers.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,16 @@ def register_cli_credentials_for_region(region, iam_role):
2525

2626
def run_pcluster_command(*args, **kwargs):
2727
"""Run a command after assuming the role configured through register_cli_credentials_for_region."""
28+
2829
region = kwargs.get("region")
2930
if not region:
3031
region = os.environ["AWS_DEFAULT_REGION"]
3132

3233
if region in cli_credentials:
33-
with sts_credential_provider(region, cli_credentials[region]):
34+
with sts_credential_provider(
35+
region, credential_arn=kwargs.get("custom_cli_credentials") or cli_credentials.get(region)
36+
):
37+
kwargs.pop("custom_cli_credentials", None)
3438
return run_command(*args, **kwargs)
3539
else:
3640
return run_command(*args, **kwargs)

tests/integration-tests/tests/iam/test_iam.py

Lines changed: 131 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@
1717
import pytest
1818
import yaml
1919
from assertpy import assert_that
20+
from cfn_stacks_factory import CfnStack, CfnStacksFactory
21+
from framework.tests_configuration.config_utils import get_all_regions
2022
from remote_command_executor import RemoteCommandExecutor
2123
from s3_common_utils import check_s3_read_resource, check_s3_read_write_resource, get_policy_resources
22-
from utils import wait_for_computefleet_changed
24+
from utils import generate_stack_name, wait_for_computefleet_changed
2325

2426
from tests.common.assertions import assert_no_errors_in_logs
2527
from tests.schedulers.test_awsbatch import _test_job_submission as _test_job_submission_awsbatch
@@ -332,3 +334,131 @@ def test_s3_read_write_resource(region, pcluster_config_reader, s3_bucket_factor
332334
# Check S3 resources
333335
check_s3_read_resource(region, cluster, get_policy_resources(config, enable_write_access=False))
334336
check_s3_read_write_resource(region, cluster, get_policy_resources(config, enable_write_access=True))
337+
338+
339+
@pytest.mark.parametrize("iam_resource_prefix", ["/path-prefix/name-prefix-"])
340+
@pytest.mark.usefixtures("os", "instance")
341+
def test_iam_resource_prefix(
342+
initialize_resource_prefix_cli_creds,
343+
pcluster_config_reader,
344+
clusters_factory,
345+
test_datadir,
346+
scheduler_commands_factory,
347+
s3_bucket_factory,
348+
s3_bucket,
349+
iam_resource_prefix,
350+
):
351+
cli_credentials = initialize_resource_prefix_cli_creds(test_datadir)
352+
if cli_credentials:
353+
for region, creds in cli_credentials.items():
354+
355+
bucket_name = s3_bucket
356+
cfn_client, _, iam_client, _ = _create_boto3_clients(region)
357+
create_config, _ = _get_config_create_and_update(test_datadir)
358+
cluster_config = pcluster_config_reader(
359+
config_file=create_config, min_count=1, bucket=bucket_name, iam_resource_prefix=iam_resource_prefix
360+
)
361+
362+
cluster = clusters_factory(cluster_config, custom_cli_credentials=creds)
363+
_test_iam_resource_in_cluster(cfn_client, iam_client, cluster.name, iam_resource_prefix)
364+
365+
366+
def _split_resource_prefix(resource_prefix):
367+
"""To split Path and name prefix from Resource Prefix."""
368+
if resource_prefix:
369+
split_index = resource_prefix.rfind("/") + 1
370+
return (
371+
None
372+
if split_index == 0
373+
else resource_prefix
374+
if split_index == len(resource_prefix)
375+
else resource_prefix[:split_index],
376+
None
377+
if split_index == len(resource_prefix)
378+
else resource_prefix
379+
if split_index == 0
380+
else resource_prefix[split_index:],
381+
)
382+
return None, None
383+
384+
385+
def _check_iam_resource_prefix(resource_arn_list, iam_resource_prefix):
386+
"""Check the path and name of IAM resource ( Roles, policy and Instance profiles)."""
387+
iam_path, iam_name_prefix = _split_resource_prefix(iam_resource_prefix)
388+
for resource in resource_arn_list:
389+
if "arn:aws:iam:" in resource:
390+
if iam_path:
391+
assert_that(resource).contains(iam_path)
392+
else:
393+
assert_that(resource).contains("/parallelcluster/")
394+
if iam_name_prefix:
395+
assert_that(resource).contains(iam_name_prefix)
396+
397+
398+
def _test_iam_resource_in_cluster(cfn_client, iam_client, stack_name, iam_resource_prefix):
399+
"""Test IAM resources by checking the path and name prefix in AWS IAM and check cluster is created."""
400+
401+
# Check for cluster Status
402+
403+
assert_that(cfn_client.describe_stacks(StackName=stack_name).get("Stacks")[0].get("StackStatus")).is_equal_to(
404+
"CREATE_COMPLETE"
405+
)
406+
407+
resources = cfn_client.describe_stack_resources(StackName=stack_name)["StackResources"]
408+
resource_arn_list = []
409+
410+
for resource in resources:
411+
resource_type = resource["ResourceType"]
412+
if resource_type == "AWS::IAM::Role":
413+
414+
resource_arn_list.append(iam_client.get_role(RoleName=resource["PhysicalResourceId"])["Role"]["Arn"])
415+
resource_arn_list.extend(
416+
iam_client.list_role_policies(RoleName=resource["PhysicalResourceId"])["PolicyNames"]
417+
)
418+
if resource_type == "AWS::IAM::InstanceProfile":
419+
resource_arn_list.append(
420+
iam_client.get_instance_profile(InstanceProfileName=resource["PhysicalResourceId"])["InstanceProfile"][
421+
"Arn"
422+
]
423+
)
424+
_check_iam_resource_prefix(resource_arn_list, iam_resource_prefix)
425+
426+
427+
@pytest.fixture(scope="class")
428+
def initialize_resource_prefix_cli_creds(request):
429+
"""Create an IAM Role with Permission Boundary for testing Resource Prefix Feature."""
430+
431+
stack_factory = CfnStacksFactory(request.config.getoption("credential"))
432+
433+
def _create_resource_prefix_cli_creds(test_datadir):
434+
regions = request.config.getoption("regions") or get_all_regions(request.config.getoption("tests_config"))
435+
stack_template_path = os_lib.path.join("..", test_datadir / "user-role-iam-resource-prefix.cfn.yaml")
436+
with open(stack_template_path, encoding="utf-8") as stack_template_file:
437+
stack_template_data = stack_template_file.read()
438+
cli_creds = {}
439+
for region in regions:
440+
if request.config.getoption("iam_user_role_stack_name"):
441+
stack_name = request.config.getoption("iam_user_role_stack_name")
442+
logging.info(f"Using stack {stack_name} in region {region}")
443+
stack = CfnStack(
444+
name=stack_name, region=region, capabilities=["CAPABILITY_IAM"], template=stack_template_data
445+
)
446+
else:
447+
logging.info("Creating IAM roles for pcluster CLI")
448+
stack_name = generate_stack_name(
449+
"integ-tests-iam-rp-user-role", request.config.getoption("stackname_suffix")
450+
)
451+
stack = CfnStack(
452+
name=stack_name, region=region, capabilities=["CAPABILITY_IAM"], template=stack_template_data
453+
)
454+
455+
stack_factory.create_stack(stack)
456+
cli_creds[region] = stack.cfn_outputs["ParallelClusterUserRole"]
457+
return cli_creds
458+
459+
yield _create_resource_prefix_cli_creds
460+
461+
if not request.config.getoption("no_delete"):
462+
stack_factory.delete_all_stacks()
463+
else:
464+
logging.warning("Skipping deletion of CFN stacks because --no-delete option is set")
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
Image:
2+
Os: {{ os }}
3+
Iam:
4+
ResourcePrefix: {{ iam_resource_prefix }}
5+
HeadNode:
6+
InstanceType: {{ instance }}
7+
Networking:
8+
SubnetId: {{ public_subnet_id }}
9+
Ssh:
10+
KeyName: {{ key_name }}
11+
Iam:
12+
S3Access:
13+
- BucketName: {{ bucket }}
14+
KeyName: read_and_write/
15+
EnableWriteAccess: true
16+
Scheduling:
17+
Scheduler: {{ scheduler }}
18+
SlurmQueues:
19+
- Name: queue-0
20+
ComputeResources:
21+
- Name: compute-resource-0
22+
InstanceType: {{ instance }}
23+
MinCount: {{ min_count }}
24+
Networking:
25+
SubnetIds:
26+
- {{ private_subnet_id }}
27+
- Name: queue-1
28+
ComputeResources:
29+
- Name: compute-resource-0
30+
InstanceType: {{ instance }}
31+
MinCount: {{ min_count }}
32+
Networking:
33+
SubnetIds:
34+
- {{ private_subnet_id }}
35+
Iam:
36+
S3Access:
37+
- BucketName: {{ bucket }}
38+
KeyName: read_and_write/
39+
EnableWriteAccess: true
40+
41+

0 commit comments

Comments
 (0)