Skip to content

Commit f520059

Browse files
cg505Michaelvll
andauthored
allow specifying a VPC from another GCP project (#5143)
* allow specicifying a VPC from another GCP project * Update docs/source/reference/config.rst Co-authored-by: Zhanghao Wu <[email protected]> * add to jsonschema * add test --------- Co-authored-by: Zhanghao Wu <[email protected]>
1 parent a3dcfad commit f520059

File tree

4 files changed

+104
-29
lines changed

4 files changed

+104
-29
lines changed

docs/source/reference/config.rst

+17-2
Original file line numberDiff line numberDiff line change
@@ -621,15 +621,30 @@ rules for SkyPilot to function. If any VPC satisfies these rules, it is
621621
used. Otherwise, a new VPC named ``skypilot-vpc`` is automatically created
622622
with the minimal recommended firewall rules and will be used.
623623

624-
If this field is set, SkyPilot will use the VPC with this name. Useful for
625-
when users want to manually set up a VPC and precisely control its
624+
If this field is set, SkyPilot will use the VPC with this name. The VPC must
625+
have the :ref:`necessary firewall rules <gcp-minimum-firewall-rules>`. Useful
626+
for when users want to manually set up a VPC and precisely control its
626627
firewall rules. If no region restrictions are given, SkyPilot only
627628
provisions in regions for which a subnet of this VPC exists. Errors are
628629
thrown if VPC with this name is not found. The VPC does not get modified
629630
in any way, except when opening ports (e.g., via ``resources.ports``) in
630631
which case new firewall rules permitting public traffic to those ports
631632
will be added.
632633

634+
By default, only VPCs from the current project are used.
635+
636+
.. code-block:: yaml
637+
638+
gcp:
639+
vpc-name: my-vpc
640+
641+
To use a shared VPC from another GCP project, specify the name as ``<project ID>/<vpc name>``. For example:
642+
643+
.. code-block:: yaml
644+
645+
gcp:
646+
vpc-name: my-project-123456/default
647+
633648
.. _config-yaml-gcp-use-internal-ips:
634649

635650
``gcp.use_internal_ips``

sky/provision/gcp/config.py

+30-20
Original file line numberDiff line numberDiff line change
@@ -571,35 +571,45 @@ def get_usable_vpc_and_subnet(
571571

572572
specific_vpc_to_use = config.provider_config.get('vpc_name', None)
573573
if specific_vpc_to_use is not None:
574+
if '/' in specific_vpc_to_use:
575+
# VPC can also be specified in the format PROJECT_ID/VPC_NAME.
576+
# This enables use of shared VPCs.
577+
split_vpc_value = specific_vpc_to_use.split('/')
578+
if len(split_vpc_value) != 2:
579+
raise ValueError(f'Invalid VPC name: {specific_vpc_to_use}. '
580+
'Please specify the VPC name in the format '
581+
'PROJECT_ID/VPC_NAME.')
582+
project_id = split_vpc_value[0]
583+
specific_vpc_to_use = split_vpc_value[1]
584+
574585
vpcnets_all = _list_vpcnets(project_id,
575586
compute,
576587
filter=f'name={specific_vpc_to_use}')
577-
# On GCP, VPC names are unique, so it'd be 0 or 1 VPC found.
578-
assert (len(vpcnets_all) <=
579-
1), (f'{len(vpcnets_all)} VPCs found with the same name '
580-
f'{specific_vpc_to_use}')
581-
if len(vpcnets_all) == 1:
582-
# Skip checking any firewall rules if the user has specified a VPC.
583-
logger.info(f'Using user-specified VPC {specific_vpc_to_use!r}.')
584-
subnets = _list_subnets(project_id,
585-
region,
586-
compute,
587-
network=specific_vpc_to_use)
588-
if not subnets:
589-
_skypilot_log_error_and_exit_for_failover(
590-
'SUBNET_NOT_FOUND_FOR_VPC',
591-
f'No subnet for region {region} found for specified VPC '
592-
f'{specific_vpc_to_use!r}. '
593-
f'Check the subnets of VPC {specific_vpc_to_use!r} at '
594-
'https://console.cloud.google.com/networking/networks')
595-
return specific_vpc_to_use, subnets[0]
596-
else:
588+
if not vpcnets_all:
597589
# VPC with this name not found. Error out and let SkyPilot failover.
598590
_skypilot_log_error_and_exit_for_failover(
599591
'VPC_NOT_FOUND',
600592
f'No VPC with name {specific_vpc_to_use!r} is found. '
601593
'To fix: specify a correct VPC name.')
602594
# Should not reach here.
595+
assert False
596+
597+
# On GCP, VPC names are unique within a project.
598+
assert len(vpcnets_all) == 1, (vpcnets_all, specific_vpc_to_use)
599+
# Skip checking any firewall rules if the user has specified a VPC.
600+
logger.info(f'Using user-specified VPC {specific_vpc_to_use!r}.')
601+
subnets = _list_subnets(project_id,
602+
region,
603+
compute,
604+
network=specific_vpc_to_use)
605+
if not subnets:
606+
_skypilot_log_error_and_exit_for_failover(
607+
'SUBNET_NOT_FOUND_FOR_VPC',
608+
f'No subnet for region {region} found for specified VPC '
609+
f'{specific_vpc_to_use!r}. '
610+
f'Check the subnets of VPC {specific_vpc_to_use!r} at '
611+
'https://console.cloud.google.com/networking/networks')
612+
return specific_vpc_to_use, subnets[0]
603613

604614
subnets_all = _list_subnets(project_id, region, compute)
605615

sky/utils/schemas.py

+20-7
Original file line numberDiff line numberDiff line change
@@ -604,13 +604,6 @@ def get_cluster_schema():
604604

605605

606606
_NETWORK_CONFIG_SCHEMA = {
607-
'vpc_name': {
608-
'oneOf': [{
609-
'type': 'string',
610-
}, {
611-
'type': 'null',
612-
}],
613-
},
614607
'use_internal_ips': {
615608
'type': 'boolean',
616609
},
@@ -767,6 +760,13 @@ def get_config_schema():
767760
},
768761
'security_group_name':
769762
(_PRORPERTY_NAME_OR_CLUSTER_NAME_TO_PROPERTY),
763+
'vpc_name': {
764+
'oneOf': [{
765+
'type': 'string',
766+
}, {
767+
'type': 'null',
768+
}],
769+
},
770770
**_LABELS_SCHEMA,
771771
**_NETWORK_CONFIG_SCHEMA,
772772
},
@@ -805,6 +805,19 @@ def get_config_schema():
805805
'enable_gvnic': {
806806
'type': 'boolean'
807807
},
808+
'vpc_name': {
809+
'oneOf': [
810+
{
811+
'type': 'string',
812+
# vpc-name or project-id/vpc-name
813+
# VPC name and Project ID have -, a-z, and 0-9.
814+
'pattern': '^(?:[-a-z0-9]+/)?[-a-z0-9]+$'
815+
},
816+
{
817+
'type': 'null',
818+
}
819+
],
820+
},
808821
**_LABELS_SCHEMA,
809822
**_NETWORK_CONFIG_SCHEMA,
810823
},

tests/test_config.py

+37
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,43 @@ def test_invalid_enum_config(monkeypatch, tmp_path) -> None:
237237
assert 'Invalid config YAML' in e.value.args[0]
238238

239239

240+
def test_gcp_vpc_name_validation(monkeypatch, tmp_path) -> None:
241+
"""Test GCP vpc_name validation with valid and invalid pattern.
242+
243+
This tests the schema changes where vpc_name was moved from _NETWORK_CONFIG_SCHEMA
244+
to provider-specific schemas and pattern validation was added for GCP vpc_name.
245+
"""
246+
# Test valid vpc_name format
247+
for valid_vpc in ['my-vpc', 'project-id/my-vpc', 'project-123/vpc-456']:
248+
config_path = tmp_path / f'valid_{valid_vpc.replace("/", "_")}.yaml'
249+
config_path.open('w', encoding='utf-8').write(
250+
textwrap.dedent(f"""\
251+
gcp:
252+
vpc_name: {valid_vpc}
253+
"""))
254+
monkeypatch.setattr(skypilot_config, 'CONFIG_PATH', config_path)
255+
# Should not raise an exception
256+
skypilot_config._reload_config()
257+
assert skypilot_config.get_nested(('gcp', 'vpc_name'),
258+
None) == valid_vpc
259+
260+
# Test invalid vpc_name format with multiple slashes
261+
for invalid_vpc in [
262+
'UPPERCASE-VPC', 'project_id/my-vpc', 'invalid/path/format',
263+
'/missing-project', 'project-id/', 'project/vpc/extra'
264+
]:
265+
config_path = tmp_path / f'invalid_{invalid_vpc.replace("/", "_")}.yaml'
266+
config_path.open('w', encoding='utf-8').write(
267+
textwrap.dedent(f"""\
268+
gcp:
269+
vpc_name: {invalid_vpc}
270+
"""))
271+
monkeypatch.setattr(skypilot_config, 'CONFIG_PATH', config_path)
272+
with pytest.raises(ValueError) as e:
273+
skypilot_config._reload_config()
274+
assert 'Invalid config YAML' in e.value.args[0]
275+
276+
240277
def test_valid_num_items_config(monkeypatch, tmp_path) -> None:
241278
"""Test that the config is not loaded if the config file contains an invalid number of array items."""
242279
config_path = tmp_path / 'valid.yaml'

0 commit comments

Comments
 (0)