feat: Add new gcloud commands, API clients, and third-party libraries across various services.

This commit is contained in:
2026-01-01 20:26:35 +01:00
parent 5e23cbece0
commit a19e592eb7
25221 changed files with 8324611 additions and 0 deletions

View File

@@ -0,0 +1,60 @@
# -*- coding: utf-8 -*- #
# Copyright 2022 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Utils for GEC cluster commands."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope import exceptions as gcloud_exceptions
from googlecloudsdk.command_lib.run import flags
from googlecloudsdk.core import properties
def SetAdminUsers(messages, args, request):
"""Sets the cluster.authorization.admin_users to the user if unspecified.
Args:
messages: message module of edgecontainer cluster.
args: command line arguments.
request: API request to be issued
"""
request.cluster.authorization = messages.Authorization()
request.cluster.authorization.adminUsers = messages.ClusterUser()
if flags.FlagIsExplicitlySet(args, 'admin_users'):
request.cluster.authorization.adminUsers.username = args.admin_users
return
if properties.VALUES.auth.credential_file_override.Get() is not None:
raise gcloud_exceptions.RequiredArgumentException(
'--admin-users', 'Required if auth/credential_file_override is defined.'
)
service_account_override = (
properties.VALUES.auth.impersonate_service_account.Get()
)
if service_account_override is not None:
request.cluster.authorization.adminUsers.username = service_account_override
else:
default_account = properties.VALUES.core.account.Get()
if default_account is None:
raise gcloud_exceptions.RequiredArgumentException(
'--admin-users',
(
'Required if no account is active and'
' --impersonate-service-account is undefined.'
),
)
request.cluster.authorization.adminUsers.username = default_account

View File

@@ -0,0 +1,70 @@
# -*- coding: utf-8 -*- #
# Copyright 2025 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Utils for cluster isolation commands."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.edge_cloud.container import util
from googlecloudsdk.command_lib.run import flags
def UpdateClusterIsolation(ref, args, request):
"""Updates the Cluster Isolation mode.
If --enable-cluster-isolation flag is specified, it will be used to
update the Cluster Isolation mode.
Args:
ref: reference to the cluster object.
args: command line arguments.
request: API request to be issued.
Returns:
modified request
"""
del ref # unused argument
if not flags.FlagIsExplicitlySet(args, "enable_cluster_isolation"):
return request
release_track = args.calliope_command.ReleaseTrack()
if request.cluster is None:
request.cluster = util.GetMessagesModule(release_track).Cluster()
if args.enable_cluster_isolation.upper() == "TRUE":
request.cluster.enableClusterIsolation = True
elif args.enable_cluster_isolation.upper() == "FALSE":
request.cluster.enableClusterIsolation = False
else:
raise ValueError(
"Invalid value for --enable-cluster-isolation: %s"
% args.enable_cluster_isolation
)
_AddFieldToUpdateMask("enableClusterIsolation", request)
return request
def _AddFieldToUpdateMask(field, request):
if not request.updateMask:
request.updateMask = field
return request
if field not in request.updateMask:
request.updateMask = request.updateMask + "," + field
return request

View File

@@ -0,0 +1,79 @@
# -*- coding: utf-8 -*- #
# Copyright 2025 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Utils for container runtime configuration commands."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.edge_cloud.container import util
from googlecloudsdk.command_lib.run import flags
def UpdateContainerRuntimeclass(ref, args, request):
"""Updates the default container runtimeclass.
If --container-default-runtime-class flag is specified, it will be used to
update the default container runtimeclass.
Args:
ref: reference to the cluster object.
args: command line arguments.
request: API request to be issued.
Returns:
modified request
"""
del ref # unused argument
if not flags.FlagIsExplicitlySet(args, "container_default_runtime_class"):
return request
release_track = args.calliope_command.ReleaseTrack()
if request.cluster is None:
request.cluster = util.GetMessagesModule(release_track).Cluster()
messages = util.GetMessagesModule(release_track)
if request.cluster.containerRuntimeConfig is None:
request.cluster.containerRuntimeConfig = messages.ContainerRuntimeConfig()
container_runtime = messages.ContainerRuntimeConfig()
if args.container_default_runtime_class.upper() == "GVISOR":
request.cluster.containerRuntimeConfig.defaultContainerRuntime = (
container_runtime.DefaultContainerRuntimeValueValuesEnum.GVISOR
)
elif args.container_default_runtime_class.upper() == "RUNC":
request.cluster.containerRuntimeConfig.defaultContainerRuntime = (
container_runtime.DefaultContainerRuntimeValueValuesEnum.RUNC
)
else:
raise ValueError(
"Unsupported --container-default-runtime-class value: "
+ args.container_default_runtime_class
)
_AddFieldToUpdateMask("containerRuntimeConfig", request)
return request
def _AddFieldToUpdateMask(field, request):
if not request.updateMask:
request.updateMask = field
return request
if field not in request.updateMask:
request.updateMask = request.updateMask + "," + field
return request

View File

@@ -0,0 +1,35 @@
# -*- coding: utf-8 -*- #
# Copyright 2022 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Fallthrough hooks for edge-cloud commands."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
def GetClusterFallthrough():
"""Python hook to get the value for the '-' cluster.
See details at:
https://cloud.google.com/apis/design/design_patterns#list_sub-collections
This allows us to operate on node pools without needing to specify a specific
parent cluster.
Returns:
The value of the wildcard cluster.
"""
return '-'

View File

@@ -0,0 +1,616 @@
# -*- coding: utf-8 -*- #
# Copyright 2023 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Flags and helpers for the container related commands."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope import arg_parsers
def AddAuthProviderCmdPath(parser):
parser.add_argument(
'--auth-provider-cmd-path',
help="""
Path to the gcloud executable for the auth provider field in kubeconfig.
""",
)
def AddAdminUsers(parser):
parser.add_argument(
'--admin-users',
help="""
Username (Google email address) of the user who should be granted
cluster-admin initially. This currently supports exactly one admin. If
not set, the account issuing the creation request will be used by
default.
""",
)
def AddClusterIPV4CIDR(parser):
parser.add_argument(
'--cluster-ipv4-cidr',
default='10.0.0.0/17',
help="""
All pods in the cluster are assigned an RFC1918 IPv4 address from this
block. This field cannot be changed after creation.
""",
)
def AddServicesIPV4CIDR(parser):
parser.add_argument(
'--services-ipv4-cidr',
default='10.96.0.0/12',
help="""
All services in the cluster are assigned an RFC1918 IPv4 address from
this block. This field cannot be changed after creation.
""",
)
def AddDefaultMaxPodsPerNode(parser):
parser.add_argument(
'--default-max-pods-per-node',
help='The default maximum number of pods per node.',
)
def AddFleetProject(parser):
parser.add_argument(
'--fleet-project',
help='Name of the Fleet host project where the cluster is registered.',
)
def AddLabels(parser):
parser.add_argument(
'--labels',
metavar='KEY=VALUE',
type=arg_parsers.ArgDict(),
help="""
List of label KEY=VALUE pairs to add.
Keys must start with a lowercase character and contain only hyphens
(-), underscores (```_```), lowercase characters, and numbers. Values must
contain only hyphens (-), underscores (```_```), lowercase characters, and
numbers.
""",
)
def AddMaintenanceWindowRecurrence(parser):
parser.add_argument(
'--maintenance-window-recurrence',
help="""
An RFC 5545 (https://tools.ietf.org/html/rfc5545#section-3.8.5.3)
recurrence rule for how the cluster maintenance window recurs. They go
on for the span of time between the start and the end time. E.g.
FREQ=WEEKLY;BYDAY=SU.
""",
)
def AddMaintenanceWindowEnd(parser):
parser.add_argument(
'--maintenance-window-end',
help="""
End time of the recurring cluster maintenance window in the RFC 3339
(https://www.rfc-editor.org/rfc/rfc3339.txt) format. E.g.
"2021-01-01T00:00:00Z" or "2021-01-01T00:00:00-05:00"
""",
)
def AddMaintenanceWindowStart(parser):
parser.add_argument(
'--maintenance-window-start',
help="""
Start time of the recurring cluster maintenance window in the RFC 3339
(https://www.rfc-editor.org/rfc/rfc3339.txt) format. E.g.
"2021-01-01T00:00:00Z" or "2021-01-01T00:00:00-05:00"
""",
)
def AddClusterIPV6CIDR(parser):
parser.add_argument(
'--cluster-ipv6-cidr',
help="""
If specified, all pods in the cluster are assigned an RFC4193 IPv6 address
from this block. This field cannot be changed after creation.
""",
)
def AddServicesIPV6CIDR(parser):
parser.add_argument(
'--services-ipv6-cidr',
help="""
If specified, all services in the cluster are assigned an RFC4193 IPv6
address from this block. This field cannot be changed after creation.
""",
)
def AddControlPlaneKMSKey(parser):
parser.add_argument(
'--control-plane-kms-key',
help="""
Google Cloud KMS key that will be used to secure persistent disks of the
control plane VMs of a remote control plane cluster. The Edge Container
service account for this project must have
`roles/cloudkms.cryptoKeyEncrypterDecrypter` on the key.
If not provided, a Google-managed key will be used by default.
""",
)
def AddSystemAddonsConfig(parser):
parser.add_argument(
'--system-addons-config',
type=arg_parsers.YAMLFileContents(),
help="""
If specified as a YAML/JSON file, customized configuration in this file
will be applied to the system add-ons.
For example,
{
"systemAddonsConfig": {
"ingress": {
"disabled": true,
"ipv4_vip": "10.0.0.1"
}
}
}
""",
)
def AddExternalLbIpv4AddressPools(parser):
parser.add_argument(
'--external-lb-ipv4-address-pools',
type=arg_parsers.ArgList(),
metavar='EXTERNAL_LB_IPV4_ADDRESS',
help="""
IPv4 address pools that are used for data plane load balancing of
local control plane clusters. Existing pools cannot be updated
after cluster creation; only adding new pools is allowed.
Each address pool must be specified as one of the following
two types of values:
1. A IPv4 address range, for example, "10.0.0.1-10.0.0.10". A range that contains a single IP (e.g. "10.0.0.1-10.0.0.1") is allowed.
2. A IPv4 CIDR block, for example, "10.0.0.1/24"
Use comma when specifying multiple address pools, for example:
--external-lb-ipv4-address-pools 10.0.0.1-10.0.0.10,10.0.0.1/24
""",
)
def AddExternalLbIpv6AddressPools(parser):
parser.add_argument(
'--external-lb-ipv6-address-pools',
type=arg_parsers.ArgList(),
metavar='EXTERNAL_LB_IPV6_ADDRESS',
help="""
IPv6 address pools that are used for data plane load balancing of
local control plane clusters. Existing pools cannot be updated
after cluster creation; only adding new pools is allowed.
Each address pool must be specified as one of the following
two types of values:
1. A IPv6 address range, for example, "2001:db8::1-2001:db8::a". A range that contains a single IP (e.g. "2001:db8::1-2001:db8::1") is allowed.
2. A IPv6 CIDR block, for example, "2001:db8::/120"
Use comma when specifying multiple address pools, for example:
--external-lb-ipv6-address-pools 2001:db8::1-2001:db8::a,2001:db8::/120
""",
)
def AddExternalLoadBalancerAddressPools(parser):
"""Adds external load balancer address pools."""
external_lb_config_address_pools_help_text = """
Path to a YAML/JSON file containing external load balancer pool configuration.
External load balancer pools are used for data plane load balancing of
local control plane clusters, with custom config such as address pool
name. Either --external-lb-ipv4-address-pools or --external-lb-address-pools
should be specified.
Existing pools cannot be updated after cluster creation; only adding new
pools is allowed currently.
For example,
```
{
"externalLoadBalancerAddressPools": [
{
"addressPool": "MyLoadBalancerPool",
"ipv4Range": ["10.200.0.200-10.200.0.204","10.200.0.300/30"],
"avoidBuggyIps": "false",
"manualAssign": "true"
}
]
}
```
*address_pool*::: Optional. A name that identifies an address pool. If a name is not specified, an auto-generated one will be used.
*ipv4_range*::: Mandatory. One or more ipv4 address range, each must be specified as one
of the following two types of values:
1. A IPv4 address range, for example, "10.0.0.1-10.0.0.10". A range that contains a single IP (e.g. "10.0.0.1-10.0.0.1") is allowed.
2. A IPv4 CIDR block, for example, "10.0.0.1/24"
*ipv6_range*::: Optional. One or more ipv6 address range, each must be specified as one
of the following two types of values:
1. A IPv6 address range, for example, "2001:db8::1-2001:db8::a". A range that contains a single IP (e.g. "2001:db8::1-2001:db8::1") is allowed.
2. A IPv6 CIDR block, for example, "2001:db8::/120"
*avoid_buggy_ips*::: Optional. If true, the pool omits IP addresses
ending in .0 and .255. Some network hardware drops traffic to these
special addresses.
Its default value is false.
*manual_assign*::: Optional. If true, addresses in this pool are not
automatically assigned to Kubernetes Services. If true, an IP address in
this pool is used only when it is specified explicitly by a service.
Its default value is false.
"""
parser.add_argument(
'--external-lb-address-pools',
help=external_lb_config_address_pools_help_text,
type=arg_parsers.YAMLFileContents(),
)
def AddControlPlaneNodeLocation(parser):
parser.add_argument(
'--control-plane-node-location',
help="""
Google Edge Cloud zone where the local control plane nodes
will be created.
""",
)
def AddControlPlaneNodeCount(parser):
parser.add_argument(
'--control-plane-node-count',
help="""
The number of local control plane nodes in a cluster. Use one to create
a single-node control plane or use three to create a high availability
control plane.
Any other numbers of nodes will not be accepted.
""",
)
def AddControlPlaneMachineFilter(parser):
parser.add_argument(
'--control-plane-machine-filter',
help="""
Only machines matching this filter will be allowed to host
local control plane nodes.
The filtering language accepts strings like "name=<name>",
and is documented here: [AIP-160](https://google.aip.dev/160).
""",
)
def AddControlPlaneSharedDeploymentPolicy(parser):
parser.add_argument(
'--control-plane-shared-deployment-policy',
help="""
Policy configuration about how user application is deployed for
local control plane cluster. It supports two values, ALLOWED and
DISALLOWED. ALLOWED means that user application can be deployed on
control plane nodes. DISALLOWED means that user application can not be
deployed on control plane nodes. Instead, it can only be deployed on
worker nodes. By default, this value is DISALLOWED. The input is case
insensitive.
""",
)
def AddControlPlaneNodeStorageSchema(parser):
parser.add_argument(
'--control-plane-node-storage-schema',
help="""
Name for the storage schema of control plane nodes.
""",
)
def AddControlPlaneNodeSystemPartitionSize(parser):
parser.add_argument(
'--control-plane-node-system-partition-size-gib',
hidden=True,
type=int,
choices=[100, 300],
help="""
Specifies the system partition size in GiB for the control plane nodes.
This parameter is optional. Valid values are 100 and 300 to set the system
partition size to 100GiB and 300GiB, respectively. If this parameter is
not specified, the system partition is created using the default size
specified in the system storage schema applicable to the control plane
nodes.
""",
)
def AddLROMaximumTimeout(parser):
parser.add_argument(
'--lro-timeout',
help="""
Overwrite the default LRO maximum timeout.
""",
)
def AddVersion(parser):
parser.add_argument(
'--version',
help="""
Target cluster version. For example: "1.5.0".
""",
)
def AddReleaseChannel(parser):
parser.add_argument(
'--release-channel',
default='RELEASE_CHANNEL_UNSPECIFIED',
help="""
Release channel a cluster is subscribed to. It supports two values,
NONE and REGULAR. NONE is used to opt out of any release channel. Clusters
subscribed to the REGULAR channel will be automatically upgraded to
versions that are considered GA quality, and cannot be manually upgraded.
Additionally, if the REGULAR channel is used, a specific target version
cannot be set with the 'version' flag. If left unspecified, the release
channel will default to REGULAR.
""",
)
def AddUpgradeVersion(parser):
parser.add_argument(
'--version',
required=True,
help="""
Target cluster version to upgrade to. For example: "1.5.1".
""",
)
def AddUpgradeSchedule(parser):
parser.add_argument(
'--schedule',
required=True,
help="""
Schedule to upgrade a cluster after the request is acknowledged by Google.
Support values: IMMEDIATELY.
""",
)
def AddOfflineCredential(parser):
parser.add_argument(
'--offline-credential',
action='store_true',
help="""
Once specified, an offline credential will be generated for the cluster.
""",
)
def AddUseGoogleManagedKey(parser):
parser.add_argument(
'--use-google-managed-key',
action='store_true',
help="""
Once specified, a Google-managed key will be used for the control plane
disk encryption.
""",
)
def AddNodeCount(parser, required=True):
parser.add_argument(
'--node-count',
required=required,
help="""
Default nodeCount used by this node pool.
""",
)
def AddNodeLocation(parser):
parser.add_argument(
'--node-location',
required=True,
help="""
Google Edge Cloud zone where nodes in this node pool will be created.
""",
)
def AddMachineFilter(parser):
parser.add_argument(
'--machine-filter',
help="""
Only machines matching this filter will be allowed to join the node
pool. The filtering language accepts strings like "name=<name>", and is
documented in more detail at https://google.aip.dev/160.
""",
)
def AddLocalDiskKMSKey(parser):
parser.add_argument(
'--local-disk-kms-key',
help="""
Google Cloud KMS key that will be used to secure local disks on nodes
in this node pool. The Edge Container service account for this project
must have `roles/cloudkms.cryptoKeyEncrypterDecrypter` on the key.
If not provided, a Google-managed key will be used instead.
""",
)
def AddNodeLabelsForCreateNodePool(parser):
parser.add_argument(
'--node-labels',
help="""
Comma-delimited list of key-value pairs that comprise labels for the
individual nodes in the node pool. This flag sets the Kubernetes
labels, unlike `--labels` which sets the cloud resource labels.
""",
metavar='KEY=VALUE',
type=arg_parsers.ArgDict(),
)
def AddNodeLabelsForUpdateNodePool(parser):
parser.add_argument(
'--node-labels',
help="""
Comma-delimited list of key-value pairs that comprise labels for the
individual nodes in the node pool. This flag updates the Kubernetes
labels, unlike `--update-labels`, `--remove-labels`, and `--clear-labels`
which update the cloud resource labels.
""",
metavar='KEY=VALUE',
type=arg_parsers.ArgDict(),
)
def AddNodeStorageSchema(parser):
parser.add_argument(
'--node-storage-schema',
help="""
Name for the storage schema of worker nodes.
""",
)
def AddNodeSystemPartitionSize(parser):
parser.add_argument(
'--node-system-partition-size-gib',
hidden=True,
type=int,
choices=[100, 300],
help="""
Specifies the system partition size in GiB for the worker nodes. This
parameter is optional. Valid values are 100 and 300 to set the system
partition size to 100GiB and 300GiB, respectively. If this parameter is
not specified, the system partition is created using the default size
specified in the system storage schema applicable to the worker nodes.
""",
)
def AddOfflineRebootTtL(parser):
parser.add_argument(
'--offline-reboot-ttl',
type=arg_parsers.Duration(),
help="""
Specifies the maximum duration a node can reboot offline (without
connection to Google) and then rejoin its cluster to resume its
designated workloads. This duration is relative to the machine's most
recent connection to Google. The maximum allowed duration is 7 days.
To disallow offline reboot, set the duration to "PT0S". The parameter
should be an ISO 8601 duration string, for example, "P1DT1H2M3S".
""",
)
def AddZoneStorageKMSKey(parser):
parser.add_argument(
'--zone-storage-kms-key',
help="""
Google Cloud KMS key that will be used to encrypt and decrypt the root key
for zone storage encryption. The zone storage KMS key is only
applicable to the storage infra cluster. The Edge Container service
account for this project must have
`roles/cloudkms.cryptoKeyEncrypterDecrypter` on the key.
If not provided, a Google-managed key will be used by default.
""",
)
def AddEnableRobinCNS(parser):
parser.add_argument(
'--enable-robin-cns',
action='store_true',
hidden=True,
help="""
If set, Robin CNS will be enabled on the cluster.
WARNING:
Enabling Robin CNS is irreversible. Once enabled, it cannot be disabled.
Enabling Robin CNS will take over all unused local Persistent Volumes (PVs)
in the cluster. Any data on these PVs will be permanently lost.
""",
)
def AddContainerDefaultRuntimeClass(parser):
parser.add_argument(
'--container-default-runtime-class',
help="""
Name of the default runtime class for containers. It supports two values
RUNC and GVISOR.
""",
)
def AddEnableClusterIsolation(parser):
parser.add_argument(
'--enable-cluster-isolation',
help="""
If set, the cluster will be created in a secure cluster isolation mode.
""",
)
def AddEnableGoogleGroupAuthentication(parser):
parser.add_argument(
'--enable-google-group-authentication',
action='store_true',
help="""
If set, the cluster will be configured to use Google Group authentication.
""",
)
def AddEnableRemoteBackup(parser):
parser.add_argument(
'--enable-remote-backup',
action='store_true',
hidden=True,
help="""
If set, the cluster will be created with remote backup featureenabled.
""",
)

View File

@@ -0,0 +1,39 @@
# -*- coding: utf-8 -*- #
# Copyright 2022 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Utils for GKE Hub memberships commands."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.edge_cloud.container import util
from googlecloudsdk.command_lib.run import flags
def SetFleetProjectPath(ref, args, request):
"""Sets the cluster.fleet.project field with a relative resource path.
Args:
ref: reference to the projectsId object.
args: command line arguments.
request: API request to be issued
"""
release_track = args.calliope_command.ReleaseTrack()
msgs = util.GetMessagesModule(release_track)
if flags.FlagIsExplicitlySet(args, 'fleet_project'):
request.cluster.fleet = msgs.Fleet()
request.cluster.fleet.project = 'projects/' + args.fleet_project
else:
request.cluster.fleet = msgs.Fleet()
request.cluster.fleet.project = 'projects/' + ref.projectsId

View File

@@ -0,0 +1,160 @@
# -*- coding: utf-8 -*- #
# Copyright 2024 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Utils for cluster maintenance window and maintenance exclusion window commands."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.edge_cloud.container import util
from googlecloudsdk.calliope import exceptions
from googlecloudsdk.command_lib.run import flags
def UpdateKmsKey(ref, args, request):
"""Updates the cluster.control_plane_encryption if --control-plane-kms-key flag is specified.
Args:
ref: reference to the cluster object.
args: command line arguments.
request: API request to be issued
Returns:
modified request
"""
del ref # unused argument
if not flags.FlagIsExplicitlySet(args, "control_plane_kms_key"):
return request
release_track = args.calliope_command.ReleaseTrack()
if request.cluster is None:
request.cluster = util.GetMessagesModule(release_track).Cluster()
if request.cluster.controlPlaneEncryption is None:
messages = util.GetMessagesModule(release_track)
request.cluster.controlPlaneEncryption = messages.ControlPlaneEncryption()
request.cluster.controlPlaneEncryption.kmsKey = args.control_plane_kms_key
_AddFieldToUpdateMask("controlPlaneEncryption", request)
return request
def UseGoogleManagedKey(ref, args, request):
"""Clears cluster.control_plane_encryption in the request if --use-google-managed-key flag is specified.
Args:
ref: reference to the cluster object.
args: command line arguments.
request: API request to be issued
Returns:
modified request
"""
del ref # unused argument
if not flags.FlagIsExplicitlySet(args, "use_google_managed_key"):
return request
if not args.use_google_managed_key:
raise exceptions.BadArgumentException(
"--no-use-google-managed-key", "The flag is not supported"
)
# TODO(b/364915328): Will complete test this after flag is enabled in GA.
if request.cluster is None:
release_track = args.calliope_command.ReleaseTrack()
request.cluster = util.GetMessagesModule(release_track).Cluster()
request.cluster.controlPlaneEncryption = None
_AddFieldToUpdateMask("controlPlaneEncryption", request)
return request
def UpdateZoneKmsKey(ref, args, request):
"""Updates the cluster.zone_storage_encryption if --zone-storage-kms-key flag is specified.
Args:
ref: reference to the cluster object.
args: command line arguments.
request: API request to be issued
Returns:
modified request
"""
del ref # unused argument
if not flags.FlagIsExplicitlySet(args, "zone_storage_kms_key"):
return request
release_track = args.calliope_command.ReleaseTrack()
if request.cluster is None:
request.cluster = util.GetMessagesModule(release_track).Cluster()
if request.cluster.zoneStorageEncryption is None:
messages = util.GetMessagesModule(release_track)
request.cluster.zoneStorageEncryption = messages.ZoneStorageEncryption()
request.cluster.zoneStorageEncryption.kmsKey = args.zone_storage_kms_key
_AddFieldToUpdateMask("zoneStorageEncryption", request)
return request
def UseGoogleManagedZoneKey(ref, args, request):
"""Clears cluster.zone_storage_encryption in the request if --use-google-managed-zone-key flag is specified.
Args:
ref: reference to the cluster object.
args: command line arguments.
request: API request to be issued
Returns:
modified request
"""
del ref # unused argument
if not flags.FlagIsExplicitlySet(args, "use_google_managed_zone_key"):
return request
if not args.use_google_managed_zone_key:
raise exceptions.BadArgumentException(
"--no-use-google-managed-zone-key", "The flag is not supported"
)
if request.cluster is None:
release_track = args.calliope_command.ReleaseTrack()
request.cluster = util.GetMessagesModule(release_track).Cluster()
request.cluster.zoneStorageEncryption = None
_AddFieldToUpdateMask("zoneStorageEncryption", request)
return request
def _AddFieldToUpdateMask(field, request):
if not request.updateMask:
request.updateMask = field
return request
if field not in request.updateMask:
request.updateMask = request.updateMask + "," + field
return request

View File

@@ -0,0 +1,683 @@
# -*- coding: utf-8 -*- #
# Copyright 2022 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Utilities for generating kubeconfig entries."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import base64
import os
import subprocess
import sys
from googlecloudsdk.core import config
from googlecloudsdk.core import exceptions as core_exceptions
from googlecloudsdk.core import log
from googlecloudsdk.core import yaml
from googlecloudsdk.core.util import encoding
from googlecloudsdk.core.util import files as file_utils
from googlecloudsdk.core.util import platforms
COMMAND_DESCRIPTION = """
Fetch credentials for a running {kind} cluster.
This command updates a kubeconfig file with appropriate credentials and
endpoint information to point kubectl at a specific {kind} cluster.
By default, credentials are written to ``HOME/.kube/config''.
You can provide an alternate path by setting the ``KUBECONFIG'' environment
variable. If ``KUBECONFIG'' contains multiple paths, the first one is used.
This command enables switching to a specific cluster, when working
with multiple clusters. It can also be used to access a previously created
cluster from a new workstation.
The command will configure kubectl to automatically refresh its
credentials using the same identity as the gcloud command-line tool.
See [](https://cloud.google.com/kubernetes-engine/docs/kubectl) for
kubectl documentation.
"""
COMMAND_EXAMPLE = """
To get credentials of a cluster named ``my-cluster'' managed in location ``us-west1'',
run:
$ {command} my-cluster --location=us-west1
"""
class Error(core_exceptions.Error):
"""Class for errors raised by edgecontainer kubeconfig utilities."""
class MissingEnvVarError(Error):
"""An exception raised when required environment variables are missing."""
def GenerateContext(project_id, location, cluster_id):
"""Generates a kubeconfig context for a Edge Container cluster.
Args:
project_id: str, project ID associated with the cluster.
location: str, Google location of the cluster.
cluster_id: str, ID of the cluster.
Returns:
The context for the kubeconfig entry.
"""
template = 'edgecontainer_{project_id}_{location}_{cluster_id}'
return template.format(
project_id=project_id, location=location, cluster_id=cluster_id)
def GenerateAuthProviderCmdArgs(track, cluster_id, project_id, location):
"""Generates command arguments for kubeconfig's authorization provider.
Args:
track: ReleaseTrack of gcloud command.
cluster_id: str, ID of the cluster.
project_id: str, ID of the project of the cluster.
location: str, Google location of the cluster.
Returns:
The command arguments for kubeconfig's authorization provider.
"""
template = (
'{prefix}edge-cloud container clusters print-access-token '
'{cluster_id} --project={project_id} --location={location}'
)
return template.format(
prefix='' if track.prefix is None else track.prefix + ' ',
cluster_id=cluster_id,
project_id=project_id,
location=location,
)
def GenerateExecAuthCmdArgs(cluster_id, project_id, location):
# type: (str, str, str) -> Sequence[str]
"""Returns exec auth provider command args."""
return [
'--use_edge_cloud',
'--project',
project_id,
'--location',
location,
'--cluster',
cluster_id,
]
def GenerateKubeconfigForOfflineCredential(cluster, context, credential_resp):
"""Generates a kubeconfig entry based on offline credential for a Edge Container cluster.
Args:
cluster: object, Edge Container cluster.
context: str, context for the kubeconfig entry.
credential_resp: Response from GetOfflineCredential API.
Raises:
Error: don't have the permission to open kubeconfig file
"""
kubeconfig = Kubeconfig.Default()
kubeconfig_for_output = EmptyKubeconfig()
# Use the same key for context, cluster, and user.
kubeconfig.contexts[context] = Context(context, context, context)
kubeconfig_for_output['contexts'].append(Context(context, context, context))
user_kwargs = {}
if credential_resp.clientKey is None:
log.error('Offline credential is missing client key.')
else:
user_kwargs['key_data'] = _GetPemDataForKubeconfig(
credential_resp.clientKey
)
if credential_resp.clientCertificate is None:
log.error('Offline credential is missing client certificate.')
else:
user_kwargs['cert_data'] = _GetPemDataForKubeconfig(
credential_resp.clientCertificate
)
user = User(context, **user_kwargs)
del user['user']['exec']
kubeconfig.users[context] = user
kubeconfig_for_output['users'].append(user)
port = getattr(cluster, 'port', 443)
if port is None:
port = 443
cluster_kwargs = {}
if cluster.clusterCaCertificate is None:
log.warning('Cluster is missing certificate authority data.')
else:
cluster_kwargs['ca_data'] = _GetPemDataForKubeconfig(
cluster.clusterCaCertificate
)
kubeconfig.clusters[context] = Cluster(
context, 'https://{}:{}'.format(cluster.endpoint, port), **cluster_kwargs
)
kubeconfig_for_output['clusters'].append(
Cluster(
context,
'https://{}:{}'.format(cluster.endpoint, port),
**cluster_kwargs
)
)
kubeconfig.SetCurrentContext(context)
kubeconfig_for_output['current-context'] = context
yaml.dump(kubeconfig_for_output, sys.stderr)
kubeconfig.SaveToFile()
log.status.Print(
'A new kubeconfig entry "{}" has been generated and set as the '
'current context.'.format(context)
)
def GenerateKubeconfig(cluster, context, cmd_path, cmd_args, exec_args):
"""Generates a kubeconfig entry for a Edge Container cluster.
Args:
cluster: object, Edge Container cluster.
context: str, context for the kubeconfig entry.
cmd_path: str, authentication provider command path.
cmd_args: str, authentication provider command arguments.
exec_args: str, exec auth command arguments.
Raises:
Error: don't have the permission to open kubeconfig file
"""
kubeconfig = Kubeconfig.Default()
# Use the same key for context, cluster, and user.
kubeconfig.contexts[context] = Context(context, context, context)
user_kwargs = {
'auth_provider': 'gcp',
'auth_provider_cmd_path': cmd_path,
'auth_provider_cmd_args': cmd_args,
'auth_provider_expiry_key': '{.expireTime}',
'auth_provider_token_key': '{.accessToken}',
'exec_auth_args': exec_args
}
user = User(context, **user_kwargs)
kubeconfig.users[context] = user
cluster_kwargs = {}
if cluster.clusterCaCertificate is None:
log.warning('Cluster is missing certificate authority data.')
else:
cluster_kwargs['ca_data'] = _GetPemDataForKubeconfig(
cluster.clusterCaCertificate
)
# Note that we use port 6443 for RCP clusters if not specified, and we rely
# on the cluster.port for LCP clusters (default: 6443).
port = getattr(cluster, 'port', 6443)
if port is None:
port = 6443
kubeconfig.clusters[context] = Cluster(
context, 'https://{}:{}'.format(cluster.endpoint, port), **cluster_kwargs)
kubeconfig.SetCurrentContext(context)
kubeconfig.SaveToFile()
log.status.Print(
'A new kubeconfig entry "{}" has been generated and set as the '
'current context.'.format(context))
def _GetPemDataForKubeconfig(pem):
# Field cert/key data in kubeconfig
# expects a base64 encoded string of a PEM.
return base64.b64encode(pem.encode('utf-8')).decode('utf-8')
# kubeconfig code lifted from googlecloudsdk/api_lib/container/kubeconfig.py
# with a few tweaks.
class Kubeconfig(object):
"""Interface for interacting with a kubeconfig file."""
def __init__(self, raw_data, filename):
self._filename = filename
self._data = raw_data
self.clusters = {}
self.users = {}
self.contexts = {}
for cluster in self._data['clusters']:
self.clusters[cluster['name']] = cluster
for user in self._data['users']:
self.users[user['name']] = user
for context in self._data['contexts']:
self.contexts[context['name']] = context
@property
def current_context(self):
return self._data['current-context']
@property
def filename(self):
return self._filename
def Clear(self, key):
self.contexts.pop(key, None)
self.clusters.pop(key, None)
self.users.pop(key, None)
if self._data.get('current-context') == key:
self._data['current-context'] = ''
def SaveToFile(self):
"""Save kubeconfig to file.
Raises:
Error: don't have the permission to open kubeconfig file.
"""
self._data['clusters'] = list(self.clusters.values())
self._data['users'] = list(self.users.values())
self._data['contexts'] = list(self.contexts.values())
with file_utils.FileWriter(self._filename, private=True) as fp:
yaml.dump(self._data, fp)
def SetCurrentContext(self, context):
self._data['current-context'] = context
@classmethod
def _Validate(cls, data):
"""Make sure we have the main fields of a kubeconfig."""
if not data:
raise Error('empty file')
try:
for key in ('clusters', 'users', 'contexts'):
if not isinstance(data[key], list):
raise Error('invalid type for {0}: {1}'.format(
data[key], type(data[key])))
except KeyError as error:
raise Error('expected key {0} not found'.format(error))
@classmethod
def LoadFromFile(cls, filename):
try:
data = yaml.load_path(filename)
except yaml.Error as error:
raise Error('unable to load kubeconfig for {0}: {1}'.format(
filename, error.inner_error))
cls._Validate(data)
return cls(data, filename)
@classmethod
def LoadOrCreate(cls, filename):
"""Read in the kubeconfig, and if it doesn't exist create one there."""
try:
return cls.LoadFromFile(filename)
except (Error, IOError) as error:
log.debug('unable to load default kubeconfig: {0}; recreating {1}'.format(
error, filename))
file_utils.MakeDir(os.path.dirname(filename))
kubeconfig = cls(EmptyKubeconfig(), filename)
kubeconfig.SaveToFile()
return kubeconfig
@classmethod
def Default(cls):
return cls.LoadOrCreate(Kubeconfig.DefaultPath())
@staticmethod
def DefaultPath():
"""Return default path for kubeconfig file."""
kubeconfig = encoding.GetEncodedValue(os.environ, 'KUBECONFIG')
if kubeconfig:
kubeconfig = kubeconfig.split(os.pathsep)[0]
return os.path.abspath(kubeconfig)
# This follows the same resolution process as kubectl for the config file.
home_dir = encoding.GetEncodedValue(os.environ, 'HOME')
if not home_dir and platforms.OperatingSystem.IsWindows():
home_drive = encoding.GetEncodedValue(os.environ, 'HOMEDRIVE')
home_path = encoding.GetEncodedValue(os.environ, 'HOMEPATH')
if home_drive and home_path:
home_dir = os.path.join(home_drive, home_path)
if not home_dir:
home_dir = encoding.GetEncodedValue(os.environ, 'USERPROFILE')
if not home_dir:
raise MissingEnvVarError(
'environment variable {vars} or KUBECONFIG must be set to store '
'credentials for kubectl'.format(
vars='HOMEDRIVE/HOMEPATH, USERPROFILE, HOME,' if platforms
.OperatingSystem.IsWindows() else 'HOME'))
return os.path.join(home_dir, '.kube', 'config')
def Merge(self, kubeconfig):
"""Merge another kubeconfig into self.
In case of overlapping keys, the value in self is kept and the value in
the other kubeconfig is lost.
Args:
kubeconfig: a Kubeconfig instance
"""
self.SetCurrentContext(self.current_context or kubeconfig.current_context)
self.clusters = dict(
list(kubeconfig.clusters.items()) + list(self.clusters.items()))
self.users = dict(list(kubeconfig.users.items()) + list(self.users.items()))
self.contexts = dict(
list(kubeconfig.contexts.items()) + list(self.contexts.items()))
def Cluster(name, server, ca_path=None, ca_data=None):
"""Generate and return a cluster kubeconfig object."""
cluster = {
'server': server,
}
if ca_path and ca_data:
raise Error('cannot specify both ca_path and ca_data')
if ca_path:
cluster['certificate-authority'] = ca_path
elif ca_data:
cluster['certificate-authority-data'] = ca_data
else:
cluster['insecure-skip-tls-verify'] = True
return {'name': name, 'cluster': cluster}
def User(name,
auth_provider=None,
auth_provider_cmd_path=None,
auth_provider_cmd_args=None,
auth_provider_expiry_key=None,
auth_provider_token_key=None,
cert_path=None,
cert_data=None,
key_path=None,
key_data=None,
exec_auth_args=None):
"""Generates and returns a user kubeconfig object.
Args:
name: str, nickname for this user entry.
auth_provider: str, authentication provider.
auth_provider_cmd_path: str, authentication provider command path.
auth_provider_cmd_args: str, authentication provider command args.
auth_provider_expiry_key: str, authentication provider expiry key.
auth_provider_token_key: str, authentication provider token key.
cert_path: str, path to client certificate file.
cert_data: str, base64 encoded client certificate data.
key_path: str, path to client key file.
key_data: str, base64 encoded client key data.
exec_auth_args: list, exec auth provider command arguments.
Returns:
dict, valid kubeconfig user entry.
Raises:
Error: if no auth info is provided (auth_provider or cert AND key)
"""
# TODO(b/70856999) Figure out what the correct behavior for client certs is.
if not (auth_provider or (cert_path and key_path) or
(cert_data and key_data)):
raise Error('either auth_provider or cert & key must be provided')
user = {}
if _UseExecAuth():
user['exec'] = _ExecAuthPlugin(exec_auth_args)
elif auth_provider:
# Setup authprovider
# if certain 'auth_provider_' fields are "present"
if (
auth_provider_cmd_path
or auth_provider_cmd_args
or auth_provider_expiry_key
or auth_provider_token_key
):
# auth-provider is being deprecated in favor of "exec" in k8s 1.25.
user['auth-provider'] = _AuthProvider(
name=auth_provider,
cmd_path=auth_provider_cmd_path,
cmd_args=auth_provider_cmd_args,
expiry_key=auth_provider_expiry_key,
token_key=auth_provider_token_key,
)
if cert_path and cert_data:
raise Error('cannot specify both cert_path and cert_data')
if cert_path:
user['client-certificate'] = cert_path
elif cert_data:
user['client-certificate-data'] = cert_data
if key_path and key_data:
raise Error('cannot specify both key_path and key_data')
if key_path:
user['client-key'] = key_path
elif key_data:
user['client-key-data'] = key_data
return {'name': name, 'user': user}
SDK_BIN_PATH_NOT_FOUND = (
'Path to sdk installation not found. Please check your installation or use '
'the `--auth-provider-cmd-path` flag to provide the path to gcloud '
'manually.')
GKE_GCLOUD_AUTH_INSTALL_HINT = (
'Install gke-gcloud-auth-plugin for use with kubectl by following '
'https://cloud.google.com/blog/products/containers-kubernetes/'
'kubectl-auth-changes-in-gke ')
GKE_GCLOUD_AUTH_PLUGIN_NOT_FOUND = (
'ACTION REQUIRED: gke-gcloud-auth-plugin, which is needed for continued '
'use of kubectl, was not found or is not executable. ' +
GKE_GCLOUD_AUTH_INSTALL_HINT)
def _ExecAuthPlugin(exec_auth_args):
"""Generate and return an exec auth plugin config.
Args:
exec_auth_args: list, exec auth provider command arguments.
Constructs an exec auth plugin config entry readable by kubectl.
This tells kubectl to call out to gke-gcloud-auth-plugin and
parse the output to retrieve access tokens to authenticate to
the kubernetes master.
Kubernetes GKE Auth Provider plugin is defined at
https://kubernetes.io/docs/reference/access-authn-authz/authentication/#client-go-credential-plugins
GKE GCloud Exec Auth Plugin code is at
https://github.com/kubernetes/cloud-provider-gcp/tree/master/cmd/gke-gcloud-auth-plugin
Returns:
dict, valid exec auth plugin config entry.
"""
command = _GetGkeGcloudPluginCommandAndPrintWarning()
exec_cfg = {
'command': command,
'args': exec_auth_args,
'apiVersion': 'client.authentication.k8s.io/v1beta1',
'installHint': GKE_GCLOUD_AUTH_INSTALL_HINT,
'provideClusterInfo': True,
}
return exec_cfg
def _AuthProvider(name='gcp',
cmd_path=None,
cmd_args=None,
expiry_key=None,
token_key=None):
"""Generates and returns an auth provider config.
Constructs an auth provider config entry readable by kubectl. This tells
kubectl to call out to a specific gcloud command and parse the output to
retrieve access tokens to authenticate to the kubernetes master.
Kubernetes gcp auth provider plugin at
https://github.com/kubernetes/kubernetes/tree/master/staging/src/k8s.io/client-go/plugin/pkg/client/auth/gcp
Args:
name: auth provider name
cmd_path: str, authentication provider command path.
cmd_args: str, authentication provider command arguments.
expiry_key: str, authentication provider expiry key.
token_key: str, authentication provider token key.
Returns:
dict, valid auth provider config entry.
Raises:
Error: Path to sdk installation not found. Please switch to application
default credentials using one of
$ gcloud config set container/use_application_default_credentials true
$ export CLOUDSDK_CONTAINER_USE_APPLICATION_DEFAULT_CREDENTIALS=true.
"""
provider = {'name': name}
if name == 'gcp':
bin_name = 'gcloud'
if platforms.OperatingSystem.IsWindows():
bin_name = 'gcloud.cmd'
if cmd_path is None:
sdk_bin_path = config.Paths().sdk_bin_path
if sdk_bin_path is None:
raise Error(SDK_BIN_PATH_NOT_FOUND)
cmd_path = os.path.join(sdk_bin_path, bin_name)
cfg = {
# Command for gcloud credential helper
'cmd-path':
cmd_path,
# Args for gcloud credential helper
'cmd-args':
cmd_args if cmd_args else 'config config-helper --format=json',
# JSONpath to the field that is the raw access token
'token-key':
token_key if token_key else '{.credential.access_token}',
# JSONpath to the field that is the expiration timestamp
'expiry-key':
expiry_key if expiry_key else '{.credential.token_expiry}'
# Note: we're omitting 'time-fmt' field, which if provided, is a
# format string of the golang reference time. It can be safely omitted
# because config-helper's default time format is RFC3339, which is the
# same default kubectl assumes.
}
provider['config'] = cfg
return provider
def _UseExecAuth():
"""Returns a bool noting if ExecAuth should be enabled."""
env_flag = 'USE_GKE_GCLOUD_AUTH_PLUGIN'
use_gke_gcloud_auth_plugin = encoding.GetEncodedValue(os.environ, env_flag)
# Allow env flag to override behavior
if use_gke_gcloud_auth_plugin is not None:
if use_gke_gcloud_auth_plugin.lower() == 'false':
return False
elif use_gke_gcloud_auth_plugin.lower() != 'true':
log.warning(
'Ignoring unsupported env value found for {}={}'.format(
env_flag, use_gke_gcloud_auth_plugin.lower()
)
)
return True
def _GetGkeGcloudPluginCommandAndPrintWarning():
"""Gets Gke Gcloud Plugin Command to be used.
Returns Gke Gcloud Plugin Command to be used. Also,
prints warning if plugin is not present or doesn't work correctly.
Returns:
string, Gke Gcloud Plugin Command to be used.
"""
bin_name = 'gke-gcloud-auth-plugin'
if platforms.OperatingSystem.IsWindows():
bin_name = 'gke-gcloud-auth-plugin.exe'
command = bin_name
# Check if command is in PATH and executable. Else, print critical(RED)
# warning as kubectl will break if command is not executable.
try:
subprocess.run([command, '--version'],
timeout=5,
check=False,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL)
_ValidateGkeGcloudPluginVersion(command)
except Exception: # pylint: disable=broad-except
# Provide SDK Full path if command is not in PATH. This helps work
# around scenarios where cloud-sdk install location is not in PATH
# as sdk was installed using other distributions methods Eg: brew
try:
# config.Paths().sdk_bin_path throws an exception in some test envs,
# but is commonly defined in prod environments
sdk_bin_path = config.Paths().sdk_bin_path
if sdk_bin_path is None:
log.critical(GKE_GCLOUD_AUTH_PLUGIN_NOT_FOUND)
else:
sdk_path_bin_name = os.path.join(sdk_bin_path, command)
subprocess.run([sdk_path_bin_name, '--version'],
timeout=5,
check=False,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL)
command = sdk_path_bin_name # update command if sdk_path_bin_name works
except Exception: # pylint: disable=broad-except
log.critical(GKE_GCLOUD_AUTH_PLUGIN_NOT_FOUND)
return command
GKE_GCLOUD_AUTH_PLUGIN_NOT_UP_TO_DATE = (
'ACTION REQUIRED: gke-gcloud-auth-plugin, which is needed for continued '
'use of kubectl needs to be updated. ' + GKE_GCLOUD_AUTH_INSTALL_HINT)
def _ValidateGkeGcloudPluginVersion(command):
# type: (str)
"""Validate Gke Gcloud Plugin Command to be used.
GDCE will depend on the newest available version, so warn customers if they
have an older version installed.
Args:
command: Gke Gcloud Plugin Command to be used.
"""
result = subprocess.run(
[command, '--help'],
timeout=5,
check=False,
capture_output=True,
text=True,
)
if ('--project string' not in result.stderr) and (
'--project string' not in result.stdout
):
log.critical(GKE_GCLOUD_AUTH_PLUGIN_NOT_UP_TO_DATE)
def Context(name, cluster, user):
"""Generate and return a context kubeconfig object."""
return {
'name': name,
'context': {
'cluster': cluster,
'user': user,
},
}
def EmptyKubeconfig():
return {
'apiVersion': 'v1',
'contexts': [],
'clusters': [],
'current-context': '',
'kind': 'Config',
'preferences': {},
'users': [],
}

View File

@@ -0,0 +1,75 @@
# -*- coding: utf-8 -*- #
# Copyright 2022 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Utilities for edge-cloud container location commands."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import collections
import re
from apitools.base.py import encoding
from googlecloudsdk.core import exceptions
_Zone = collections.namedtuple('EdgeContainerZone', ['name', 'region'])
def ReplaceResourceZoneWithRegion(ref, args, request):
"""Replaces the request.name 'locations/{zone}' with 'locations/{region}'."""
del ref, args # Unused.
request.name = re.sub(
r'(projects/[-a-z0-9]+/locations/[a-z]+-[a-z]+[0-9])[-a-z0-9]*((?:/.*)?)',
r'\1\2', request.name)
return request
def ExtractZonesFromLocations(response, _):
"""Returns the zones from a ListLocationResponse."""
for region in response:
if not region.metadata:
continue
metadata = encoding.MessageToDict(region.metadata)
for zone in metadata.get('availableZones', []):
yield _Zone(name=zone, region=region.locationId)
def ExtractZoneFromLocation(response, args):
"""Returns the argument-specified zone from a GetLocationResponse."""
metadata = encoding.MessageToDict(response.metadata)
want_zone = args.zone.split('/')[-1]
for zone_name, zone in metadata.get('availableZones', {}).items():
if zone_name == want_zone:
if 'rackTypes' in zone:
racks = zone.pop('rackTypes')
populated_rack = []
for rack, rack_type in racks.items():
# Base racks are a pair of two modified Config-1 racks containing
# aggregation switches.
if rack_type == 'BASE':
populated_rack.append(rack + ' (BASE)')
# Expansion rack type, also known as standalone racks,
# are added by customers on demand.
elif rack_type == 'EXPANSION':
populated_rack.append(rack + ' (EXPANSION)')
# Only displaying the suffix for multi-rack rack types,
# i.e. base/expansion, ignore the rest.
else:
populated_rack.append(rack)
zone['racks'] = populated_rack
return zone
raise exceptions.Error('Zone not found: {}'.format(want_zone))

View File

@@ -0,0 +1,209 @@
# -*- coding: utf-8 -*- #
# Copyright 2024 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Utils for cluster maintenance window and maintenance exclusion window commands."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.edge_cloud.container import util
from googlecloudsdk.calliope import exceptions
from googlecloudsdk.command_lib.run import flags
def RequestWithNewMaintenanceExclusion(req, messages, args):
"""Returns an update request with a new maintenance exclusion window with id, start time, and end time specified from args.
Args:
req: API request to be issued.
messages: message module of edgecontainer cluster.
args: command line arguments.
Returns:
modified request
"""
if req.cluster.maintenancePolicy is None:
req.cluster.maintenancePolicy = messages.MaintenancePolicy()
if req.cluster.maintenancePolicy.maintenanceExclusions is None:
req.cluster.maintenancePolicy.maintenanceExclusions = []
req.cluster.maintenancePolicy.maintenanceExclusions.append(
messages.MaintenanceExclusionWindow(
id=args.add_maintenance_exclusion_name,
window=messages.TimeWindow(
startTime=args.add_maintenance_exclusion_start,
endTime=args.add_maintenance_exclusion_end,
),
)
)
return req
def AddMaintenanceExclusionWindow(ref, args, request):
"""Adds a maintenance exclusion window to the cluster if relevant flags are set.
Args:
ref: reference to the cluster object.
args: command line arguments.
request: API request to be issued
Returns:
modified request
"""
del ref # unused argument
# If none are set, pass through original request.
if (
not flags.FlagIsExplicitlySet(args, "add_maintenance_exclusion_name")
and not flags.FlagIsExplicitlySet(args, "add_maintenance_exclusion_start")
and not flags.FlagIsExplicitlySet(args, "add_maintenance_exclusion_end")
):
return request
# If at least one of them is set, ensure all flags exist.
_CheckAddMaintenanceExclusionFlags(args)
release_track = args.calliope_command.ReleaseTrack()
if request.cluster is None:
request.cluster = util.GetMessagesModule(release_track).Cluster()
# Ensure the MEW name doesn't already exist
if request.cluster.maintenancePolicy:
for mew in request.cluster.maintenancePolicy.maintenanceExclusions:
if args.add_maintenance_exclusion_name == mew.id:
raise exceptions.BadArgumentException(
"--add-maintenance-exclusion-name",
"Maintenance exclusion name " + mew.id + " already exists.",
)
request = RequestWithNewMaintenanceExclusion(
request, util.GetMessagesModule(release_track), args
)
_AddFieldToUpdateMask("maintenancePolicy", request)
return request
def _CheckAddMaintenanceExclusionFlags(args):
"""Confirms all necessary flags for adding an exclusion window is set.
Args:
args: arguments passed through gcloud command
Raises:
BadArgumentException specifying missing flag
"""
if not args.add_maintenance_exclusion_name:
raise exceptions.BadArgumentException(
"--add-maintenance-exclusion-name",
"Every maintenance exclusion window must have a name.",
)
if not args.add_maintenance_exclusion_start:
raise exceptions.BadArgumentException(
"--add-maintenance-exclusion-start",
"Every maintenance exclusion window must have a start time.",
)
if not args.add_maintenance_exclusion_end:
raise exceptions.BadArgumentException(
"--add-maintenance-exclusion-end",
"Every maintenance exclusion window must have an end time.",
)
def RemoveMaintenanceExclusionWindow(ref, args, request):
"""Removes the cluster.maintenance_policy.maintenance_exclusion_window if --remove-maintenance-exclusion-window flag is specified.
Args:
ref: reference to the cluster object.
args: command line arguments.
request: API request to be issued
Returns:
modified request
"""
del ref # unused argument
if not flags.FlagIsExplicitlySet(args, "remove_maintenance_exclusion_window"):
return request
if request.cluster is None:
release_track = args.calliope_command.ReleaseTrack()
request.cluster = util.GetMessagesModule(release_track).Cluster()
if request.cluster.maintenancePolicy is None:
_AddFieldToUpdateMask("maintenancePolicy", request)
return request
for idx, mew in enumerate(
request.cluster.maintenancePolicy.maintenanceExclusions
):
if mew.id == args.remove_maintenance_exclusion_window:
i = idx
break
else:
raise exceptions.BadArgumentException(
"--remove-maintenance-exclusion-window",
"Cannot remove a maintenance exclusion window that doesn't exist.",
)
del request.cluster.maintenancePolicy.maintenanceExclusions[i]
_AddFieldToUpdateMask("maintenancePolicy", request)
return request
def ClearMaintenanceWindow(ref, args, request):
"""Clears cluster.maintenance_policy.window in the request if --clear-maintenance-window flag is specified.
Args:
ref: reference to the cluster object.
args: command line arguments.
request: API request to be issued
Returns:
modified request
"""
del ref # unused argument
if not flags.FlagIsExplicitlySet(args, "clear_maintenance_window"):
return request
if not args.clear_maintenance_window:
raise exceptions.BadArgumentException(
"--no-clear-maintenance-window", "The flag is not supported"
)
if request.cluster is None:
release_track = args.calliope_command.ReleaseTrack()
request.cluster = util.GetMessagesModule(release_track).Cluster()
if request.cluster.maintenancePolicy:
if request.cluster.maintenancePolicy.maintenanceExclusions:
raise exceptions.BadArgumentException(
"--clear-maintenance-window",
"Cannot clear a maintenance window if there are maintenance"
" exclusions.",
)
request.cluster.maintenancePolicy = None
_AddFieldToUpdateMask("maintenancePolicy", request)
return request
def _AddFieldToUpdateMask(field, request):
if not request.updateMask:
request.updateMask = field
return request
if field not in request.updateMask:
request.updateMask = request.updateMask + "," + field
return request

View File

@@ -0,0 +1,70 @@
# -*- coding: utf-8 -*- #
# Copyright 2024 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Utils for cluster maintenance window commands."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.edge_cloud.container import util
from googlecloudsdk.calliope import exceptions
from googlecloudsdk.command_lib.run import flags
def ClearMaxUnavailable(ref, args, request):
"""Clears max_unavailable_worker_nodes in the request.
If --clear-max-unavailable-worker-nodes flag is specified,
cluster.upgrade_settings.max_unavailable_worker_nodes is cleared.
Args:
ref: reference to the cluster object.
args: command line arguments.
request: API request to be issued.
Returns:
modified request
"""
del ref # unused argument
if not flags.FlagIsExplicitlySet(args, "clear_max_unavailable_worker_nodes"):
return request
if not args.clear_max_unavailable_worker_nodes:
raise exceptions.BadArgumentException(
"--no-clear-max-unavailable-worker-nodes", "The flag is not supported"
)
if request.cluster is None:
release_track = args.calliope_command.ReleaseTrack()
request.cluster = util.GetMessagesModule(release_track).Cluster()
if request.cluster.upgradeSettings is not None:
request.cluster.upgradeSettings = None
_AddFieldToUpdateMask(
"upgrade_settings.max_unavailable_worker_nodes", request
)
return request
def _AddFieldToUpdateMask(field, request):
if not request.updateMask:
request.updateMask = field
return request
if field not in request.updateMask:
request.updateMask = request.updateMask + "," + field
return request

View File

@@ -0,0 +1,45 @@
# -*- coding: utf-8 -*- #
# Copyright 2022 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Utilities for edge-cloud container location commands."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from apitools.base.py import encoding
from googlecloudsdk.core import log
def PrintWarning(response, _):
"""Print the warning in last response.
Args:
response: The last response of series api call
_: Represents unused_args
Returns:
Nested response, normally should be the resource of a LRO.
"""
json_obj = encoding.MessageToDict(response)
if json_obj['metadata'].get('warnings'):
for warning in json_obj['metadata']['warnings']:
log.warning(warning)
if 'response' in json_obj.keys():
clusters = json_obj['response']
clusters.pop('@type')
return clusters
else:
return response

View File

@@ -0,0 +1,239 @@
# -*- coding: utf-8 -*- #
# Copyright 2022 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Shared resource flags for edge-cloud container commands."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.edge_cloud.container import util
from googlecloudsdk.api_lib.util import messages as messages_util
from googlecloudsdk.calliope import exceptions
from googlecloudsdk.calliope.concepts import concepts
from googlecloudsdk.calliope.concepts import deps
from googlecloudsdk.command_lib.util.concepts import concept_parsers
from googlecloudsdk.core import properties
GDCE_SYS_ADDONS_CONFIG = 'systemAddonsConfig'
GDCE_EXTERNAL_LB_CONFIG = 'externalLoadBalancerAddressPools'
def ClusterAttributeConfig():
return concepts.ResourceParameterAttributeConfig(
name='cluster', help_text='Cluster of the {resource}.')
def LocationAttributeConfig():
return concepts.ResourceParameterAttributeConfig(
name='location', help_text='Google Cloud location for the {resource}.')
def GetLocationsListingResourceSpec():
"""Gets the location resource spec for listing resources."""
fallthroughs = [
## if location is not set, use value from
## gcloud config get-value edge_container/location
deps.ArgFallthrough('--location'),
deps.PropertyFallthrough(properties.VALUES.edge_container.location),
]
# TODO(b/332336702): Before this CL, only listing clusters supports the
# `edge_container/location` property. All other cluster commands do not use
# it. Validate if this is a bug with current behavior, and fix if so.
config = LocationAttributeConfig()
config.fallthroughs = fallthroughs
return concepts.ResourceSpec(
'edgecontainer.projects.locations',
resource_name='location',
locationsId=config,
projectsId=concepts.DEFAULT_PROJECT_ATTRIBUTE_CONFIG)
def GetClusterResourceSpec():
return concepts.ResourceSpec(
'edgecontainer.projects.locations.clusters',
resource_name='cluster',
clustersId=ClusterAttributeConfig(),
locationsId=LocationAttributeConfig(),
projectsId=concepts.DEFAULT_PROJECT_ATTRIBUTE_CONFIG)
def AddLocationOptionalResourceArgForListing(parser):
"""Adds a resource argument for an Edge Container location.
Args:
parser: The argparse parser to add the resource arg to.
"""
concept_parsers.ConceptParser.ForResource(
'--location',
GetLocationsListingResourceSpec(),
'Edge Container location {}.'.format('to list'),
required=False).AddToParser(parser)
def AddClusterResourceArg(parser, verb, positional=True):
"""Adds a resource argument for an Edge Container cluster.
Args:
parser: The argparse parser to add the resource arg to.
verb: str, the verb to describe the resource, such as 'to update'.
positional: bool, whether the argument is positional or not.
"""
name = 'cluster' if positional else '--cluster'
concept_parsers.ConceptParser.ForResource(
name,
GetClusterResourceSpec(),
'Edge Container cluster {}.'.format(verb),
required=True).AddToParser(parser)
def NodePoolAttributeConfig():
return concepts.ResourceParameterAttributeConfig(
name='nodePool', help_text='Node pool of the {resource}.')
def GetNodePoolResourceSpec():
return concepts.ResourceSpec(
'edgecontainer.projects.locations.clusters.nodePools',
resource_name='node pool',
clustersId=ClusterAttributeConfig(),
nodePoolsId=NodePoolAttributeConfig(),
locationsId=LocationAttributeConfig(),
projectsId=concepts.DEFAULT_PROJECT_ATTRIBUTE_CONFIG)
def AddNodePoolResourceArg(parser, verb):
"""Adds a resource argument for an Edge Container node pool.
Args:
parser: The argparse parser to add the resource arg to.
verb: str, the verb to describe the resource, such as 'to update'.
"""
name = 'node_pool'
concept_parsers.ConceptParser.ForResource(
name,
GetNodePoolResourceSpec(),
'Edge Container node pool {}.'.format(verb),
required=True).AddToParser(parser)
def ProcessSystemAddonsConfig(args, req):
"""Processes the cluster.system_addons_config.
Args:
args: command line arguments.
req: API request to be issued
"""
release_track = args.calliope_command.ReleaseTrack()
msgs = util.GetMessagesModule(release_track)
data = args.system_addons_config
try:
system_addons_config = messages_util.DictToMessageWithErrorCheck(
data[GDCE_SYS_ADDONS_CONFIG], msgs.SystemAddonsConfig)
except (messages_util.DecodeError, AttributeError, KeyError) as err:
raise exceptions.InvalidArgumentException(
'--system-addons-config',
'\'{}\''.format(err.args[0] if err.args else err))
req.cluster.systemAddonsConfig = system_addons_config
def SetSystemAddonsConfig(args, request):
"""Sets the cluster.system_addons_config if specified.
Args:
args: command line arguments.
request: API request to be issued
"""
if args.IsKnownAndSpecified('system_addons_config'):
ProcessSystemAddonsConfig(args, request)
def CheckAddressPoolNameUniqueness(external_lb_address_pools):
"""Checks for unique address pool names in the given list of dictionaries.
Args:
external_lb_address_pools: A list of dictionaries representing
ExternalLoadBalancerPool messages.
Returns:
str: An error message if a duplicate address pool name is found,
otherwise None.
"""
address_pool_set = set()
for pool in external_lb_address_pools:
if 'addressPool' in pool and pool['addressPool']:
if pool['addressPool'] in address_pool_set:
return f"Duplicate address pool name: {pool['addressPool']}"
address_pool_set.add(pool['addressPool'])
return None
def ProcessExternalLoadBalancerAddressPoolsConfig(args, req):
"""Processes the cluster.externalLoadBalancerAddressPools.
Args:
args: command line arguments.
req: API request to be issued
"""
release_track = args.calliope_command.ReleaseTrack()
msgs = util.GetMessagesModule(release_track)
lbdata = args.external_lb_address_pools
if not lbdata:
return
pools = lbdata.get(GDCE_EXTERNAL_LB_CONFIG)
if not pools:
return
err = CheckAddressPoolNameUniqueness(pools)
if err:
raise exceptions.InvalidArgumentException(
'--external-lb-address-pools',
f'Duplicate address pool found: {err}'
)
mpools = []
try:
for pool in pools:
mpool = messages_util.DictToMessageWithErrorCheck(
pool, msgs.ExternalLoadBalancerPool)
mpools.append(mpool)
except (messages_util.DecodeError, AttributeError, KeyError) as err:
raise exceptions.InvalidArgumentException(
'--external-lb-address-pools',
'\'{}\''.format(err.args[0] if err.args else err))
if mpools:
req.cluster.externalLoadBalancerAddressPools = mpools
def SetExternalLoadBalancerAddressPoolsConfig(args, request):
"""Sets the cluster.external_lb_address_pools if specified.
Args:
args: command line arguments.
request: API request to be issued
"""
if args.IsKnownAndSpecified('external_lb_address_pools'):
ProcessExternalLoadBalancerAddressPoolsConfig(args, request)

View File

@@ -0,0 +1,113 @@
project:
name: project
collection: edgecontainer.projects
attributes:
- &project
parameter_name: projectsId
attribute_name: project
help: The project name.
property: core/project
location:
name: location
collection: edgecontainer.projects.locations
attributes:
- *project
- &location
parameter_name: locationsId
attribute_name: location
help: The global location name.
property: edge_container/location
disable_auto_completers: false
zone:
name: zone
collection: edgecontainer.projects.locations
attributes:
- *project
- &zone
parameter_name: locationsId
attribute_name: zone
help: The name of the Edge Container zone.
disable_auto_completers: false
cluster:
name: cluster
collection: edgecontainer.projects.locations.clusters
attributes:
- *project
- *location
- &cluster
parameter_name: clustersId
attribute_name: cluster
help: Kubernetes cluster.
disable_auto_completers: false
cluster_with_default:
name: cluster
collection: edgecontainer.projects.locations.clusters
request_id_field: clusterId
attributes:
- *project
- *location
- &cluster_with_default
parameter_name: clustersId
attribute_name: cluster
help: Kubernetes cluster.
fallthroughs:
# TODO(b/231638586): figure out a solution to prevent duplicate help text.
- hook: googlecloudsdk.command_lib.edge_cloud.container.fallthrough:GetClusterFallthrough
hint: |-
provide the argument --cluster on the command line
disable_auto_completers: false
nodePool:
name: node pool
collection: edgecontainer.projects.locations.clusters.nodePools
request_id_field: nodePoolId
attributes:
- *project
- *location
- *cluster
- &nodePool
parameter_name: nodePoolsId
attribute_name: node_pool
help: Pool of Kubernetes nodes with similar properties.
disable_auto_completers: false
machine:
name: machine
collection: edgecontainer.projects.locations.machines
request_id_field: machineId
attributes:
- *project
- *location
- &machine
parameter_name: machinesId
attribute_name: machine
help: Machines represent compute entities which can assume the role of a node in a cluster.
disable_auto_completers: false
vpnConnection:
name: vpn connection
collection: edgecontainer.projects.locations.vpnConnections
request_id_field: vpnConnectionId
attributes:
- *project
- *location
- &vpnConnection
parameter_name: vpnConnectionsId
attribute_name: vpn_connection
help: VPN connection between cluster and GCP VPC.
disable_auto_completers: false
operation:
name: operation
collection: edgecontainer.projects.locations.operations
attributes:
- *project
- *location
- parameter_name: operationsId
attribute_name: operation
help: Edge-container long running operation.
disable_auto_completers: false

View File

@@ -0,0 +1,76 @@
# -*- coding: utf-8 -*- #
# Copyright 2025 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Utils for Robin CNS related commands."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.edge_cloud.container import util
from googlecloudsdk.command_lib.run import flags
from googlecloudsdk.core.console import console_io
def PromptToEnableRobinCNSConfirmation():
"""Displays warning and prompts user for confirmation."""
message = """WARNING:
Enabling Robin CNS is irreversible. Once enabled, it cannot be disabled.
Enabling Robin CNS will take over all unused local Persistent Volumes (PVs)
in the cluster. Any data on these PVs will be permanently lost."""
console_io.PromptContinue(message=message, default=False, cancel_on_no=True)
def EnableRobinCNSInRequest(req, args):
"""Set Robin CNS config in the cluster request message."""
release_track = args.calliope_command.ReleaseTrack()
messages = util.GetMessagesModule(release_track)
if req.cluster is None:
req.cluster = messages.Cluster()
if req.cluster.systemAddonsConfig is None:
req.cluster.systemAddonsConfig = messages.SystemAddonsConfig()
if req.cluster.systemAddonsConfig.robinCloudNativeStorage is None:
req.cluster.systemAddonsConfig.robinCloudNativeStorage = (
messages.RobinCloudNativeStorage()
)
req.cluster.systemAddonsConfig.robinCloudNativeStorage.enable = True
def HandleEnableRobinCNSUpdate(ref, args, request):
"""Handles the --enable-robin-cns flag for UPDATE requests."""
del ref
if not flags.FlagIsExplicitlySet(args, "enable_robin_cns"):
return request
PromptToEnableRobinCNSConfirmation()
EnableRobinCNSInRequest(request, args)
_AddFieldToUpdateMask(
"system_addons_config.robin_cloud_native_storage.enable", request
)
return request
def _AddFieldToUpdateMask(field, request):
if not request.updateMask:
request.updateMask = field
return request
if field not in request.updateMask:
request.updateMask = request.updateMask + "," + field
return request

View File

@@ -0,0 +1,83 @@
# -*- coding: utf-8 -*- #
# Copyright 2022 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Utils for VPN Connection commands."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
class DescribeVPNTableView:
"""View model for VPN connections describe."""
def __init__(self, name, create_time, cluster, vpc, state, error):
self.name = name
self.create_time = create_time
self.cluster = cluster
self.vpc = vpc
self.state = state
if error:
self.error = error
def CreateDescribeVPNTableViewResponseHook(response, args):
"""Create DescribeVPNTableView from GetVpnConnection response.
Args:
response: Response from GetVpnConnection
args: Args from GetVpnConnection
Returns:
DescribeVPNTableView
"""
del args # args not used
name = response.name
create_time = response.createTime
details = response.details
if details:
state = details.state
error = details.error
else:
state = 'STATE_UNKNOWN'
error = ''
cluster = {}
items = response.cluster.split('/')
try:
cluster['project'] = items[1]
cluster['location'] = items[3]
cluster['ID'] = items[5]
except IndexError:
pass
if response.natGatewayIp:
cluster['NAT Gateway IP'] = response.natGatewayIp
vpc = {}
items = response.vpc.split('/')
try:
vpc['project'] = items[1]
vpc['ID'] = items[5]
except IndexError:
pass
if details:
vpc['Cloud Router'] = {
'name': details.cloudRouter.name,
'region': items[3]
}
vpc['Cloud VPNs'] = details.cloudVpns
return DescribeVPNTableView(name, create_time, cluster, vpc, state, error)

View File

@@ -0,0 +1,207 @@
# -*- coding: utf-8 -*- #
# Copyright 2022 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Shared resource flags for Edgenetwork commands."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope.concepts import concepts
from googlecloudsdk.command_lib.util.concepts import concept_parsers
from googlecloudsdk.command_lib.util.concepts import presentation_specs
def LocationAttributeConfig():
return concepts.ResourceParameterAttributeConfig(
name='location', help_text='The Cloud location for the {resource}.')
def ZoneAttributeConfig(name='zone'):
return concepts.ResourceParameterAttributeConfig(
name=name,
help_text='The zone of the {resource}.',
completion_request_params={'fieldMask': 'name'},
completion_id_field='id')
def SubnetAttributeConfig(name='subnet'):
return concepts.ResourceParameterAttributeConfig(
name=name,
help_text='The subnet of the {resource}.',
completion_request_params={'fieldMask': 'name'},
completion_id_field='id')
def RouterAttributeConfig(name='router'):
return concepts.ResourceParameterAttributeConfig(
name=name,
help_text='The router of the {resource}.',
completion_request_params={'fieldMask': 'name'},
completion_id_field='id')
def NetworkAttributeConfig(name='network'):
return concepts.ResourceParameterAttributeConfig(
name=name,
help_text='The network of the {resource}.',
completion_request_params={'fieldMask': 'name'},
completion_id_field='id')
def InterconnectAttributeConfig(name='interconnect'):
return concepts.ResourceParameterAttributeConfig(
name=name,
help_text='The interconnect of the {resource}.',
completion_request_params={'fieldMask': 'name'},
completion_id_field='id')
def GetZoneResourceSpec(resource_name='zone'):
return concepts.ResourceSpec(
'edgenetwork.projects.locations.zones',
resource_name=resource_name,
zonesId=ZoneAttributeConfig(name=resource_name),
locationsId=LocationAttributeConfig(),
projectsId=concepts.DEFAULT_PROJECT_ATTRIBUTE_CONFIG,
disable_auto_completers=False)
def GetRouterResourceSpec(resource_name='router'):
return concepts.ResourceSpec(
'edgenetwork.projects.locations.zones.routers',
resource_name=resource_name,
routersId=RouterAttributeConfig(name=resource_name),
zonesId=ZoneAttributeConfig('zone'),
locationsId=LocationAttributeConfig(),
projectsId=concepts.DEFAULT_PROJECT_ATTRIBUTE_CONFIG,
disable_auto_completers=False)
def GetNetworkResourceSpec(resource_name='network'):
return concepts.ResourceSpec(
'edgenetwork.projects.locations.zones.networks',
resource_name=resource_name,
networksId=NetworkAttributeConfig(name=resource_name),
zonesId=ZoneAttributeConfig('zone'),
locationsId=LocationAttributeConfig(),
projectsId=concepts.DEFAULT_PROJECT_ATTRIBUTE_CONFIG,
disable_auto_completers=False)
def GetInterconnectResourceSpec(resource_name='interconnect'):
return concepts.ResourceSpec(
'edgenetwork.projects.locations.zones.interconnects',
resource_name=resource_name,
interconnectsId=InterconnectAttributeConfig(name=resource_name),
zonesId=ZoneAttributeConfig('zone'),
locationsId=LocationAttributeConfig(),
projectsId=concepts.DEFAULT_PROJECT_ATTRIBUTE_CONFIG,
disable_auto_completers=False)
def AddZoneResourceArg(parser, verb, positional=False):
"""Add a resource argument for a GDCE router.
Args:
parser: the parser for the command.
verb: str, the verb to describe the resource, such as 'to create'.
positional: bool, if True, means that the resource is a positional rather
than a flag.
"""
if positional:
name = 'zone'
else:
name = '--zone'
resource_specs = [
presentation_specs.ResourcePresentationSpec(
name,
GetZoneResourceSpec(),
'The zone {}.'.format(verb),
required=True)
]
concept_parsers.ConceptParser(resource_specs).AddToParser(parser)
def AddRouterResourceArg(parser, verb, positional=False):
"""Add a resource argument for a GDCE router.
Args:
parser: the parser for the command.
verb: str, the verb to describe the resource, such as 'to create'.
positional: bool, if True, means that the resource is a positional rather
than a flag.
"""
if positional:
name = 'router'
else:
name = '--router'
resource_specs = [
presentation_specs.ResourcePresentationSpec(
name,
GetRouterResourceSpec(),
'The router {}.'.format(verb),
required=True)
]
concept_parsers.ConceptParser(resource_specs).AddToParser(parser)
def AddNetworkResourceArg(parser, verb, positional=False):
"""Add a resource argument for a GDCE network.
Args:
parser: the parser for the command.
verb: str, the verb to describe the resource, such as 'to create'.
positional: bool, if True, means that the resource is a positional rather
than a flag.
"""
if positional:
name = 'network'
else:
name = '--network'
resource_specs = [
presentation_specs.ResourcePresentationSpec(
name,
GetNetworkResourceSpec(),
'The network {}.'.format(verb),
required=True)
]
concept_parsers.ConceptParser(resource_specs).AddToParser(parser)
def AddInterconnectResourceArg(parser, verb, positional=False):
"""Add a resource argument for a GDCE interconnect.
Args:
parser: the parser for the command.
verb: str, the verb to describe the resource, such as 'to create'.
positional: bool, if True, means that the resource is a positional rather
than a flag.
"""
if positional:
name = 'interconnect'
else:
name = '--interconnect'
resource_specs = [
presentation_specs.ResourcePresentationSpec(
name,
GetInterconnectResourceSpec(),
'The interconnect {}.'.format(verb),
required=True)
]
concept_parsers.ConceptParser(resource_specs).AddToParser(parser)

View File

@@ -0,0 +1,131 @@
# -*- coding: utf-8 -*- #
# Copyright 2022 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Utils for Distributed Cloud Edge Network commands."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.core import resources
def SetResourcesPathForSubnet(ref, args, request):
"""Sets the subnet.network field with a relative resource path.
Args:
ref: reference to the subnet object.
args: command line arguments.
request: API request to be issued
Returns:
modified request
"""
# Skips if full path of the network resource is provided.
if 'projects/' in args.network:
return request
network = resources.REGISTRY.Create(
'edgenetwork.projects.locations.zones.networks',
projectsId=ref.projectsId,
locationsId=ref.locationsId,
zonesId=ref.zonesId,
networksId=args.network)
request.subnet.network = network.RelativeName()
return request
def SetResourcesPathForRouter(ref, args, request):
"""Sets the router.network field with a relative resource path.
Args:
ref: reference to the router object.
args: command line arguments.
request: API request to be issued
Returns:
modified request
"""
# Skips if full path of the network resource is provided.
if 'projects/' in args.network:
return request
network = resources.REGISTRY.Create(
'edgenetwork.projects.locations.zones.networks',
projectsId=ref.projectsId,
locationsId=ref.locationsId,
zonesId=ref.zonesId,
networksId=args.network)
request.router.network = network.RelativeName()
return request
def SetResourcesPathForRoute(ref, args, request):
"""Sets the route.network field with a relative resource path.
Args:
ref: reference to the route object.
args: command line arguments.
request: API request to be issued
Returns:
modified request
"""
# Skips if full path of the network resource is provided.
if 'projects/' in args.network:
return request
network = resources.REGISTRY.Create(
'edgenetwork.projects.locations.zones.networks',
projectsId=ref.projectsId,
locationsId=ref.locationsId,
zonesId=ref.zonesId,
networksId=args.network)
request.route.network = network.RelativeName()
return request
def SetResourcesPathForAttachment(ref, args, request):
"""Sets the interconnectAttachment.router and interconnectAttachment.interconnect field with a relative resource path.
Args:
ref: reference to the interconnectAttachment object.
args: command line arguments.
request: API request to be issued
Returns:
modified request
"""
if 'projects/' not in args.interconnect:
interconnect = resources.REGISTRY.Create(
'edgenetwork.projects.locations.zones.interconnects',
projectsId=ref.projectsId,
locationsId=ref.locationsId,
zonesId=ref.zonesId,
interconnectsId=args.interconnect)
request.interconnectAttachment.interconnect = interconnect.RelativeName()
if args.network and 'projects/' not in args.network:
network = resources.REGISTRY.Create(
'edgenetwork.projects.locations.zones.networks',
projectsId=ref.projectsId,
locationsId=ref.locationsId,
zonesId=ref.zonesId,
networksId=args.network)
request.interconnectAttachment.network = network.RelativeName()
return request

View File

@@ -0,0 +1,128 @@
project:
name: project
collection: edgenetwork.projects
attributes:
- &project
parameter_name: projectsId
attribute_name: project
help: The project name.
property: core/project
location:
name: location
collection: edgenetwork.projects.locations
attributes:
- *project
- &location
parameter_name: locationsId
attribute_name: location
help: The global location name.
disable_auto_completers: false
zone:
name: zone
collection: edgenetwork.projects.locations.zones
request_id_field: zoneId
attributes:
- *project
- *location
- &zone
parameter_name: zonesId
attribute_name: zone
help: The name of the Google Distributed Cloud Edge zone.
disable_auto_completers: false
network:
name: network
collection: edgenetwork.projects.locations.zones.networks
request_id_field: networkId
attributes:
- *project
- *location
- *zone
- &network
parameter_name: networksId
attribute_name: network
help: Google Distributed Cloud Edge network.
disable_auto_completers: false
subnet:
name: subnet
collection: edgenetwork.projects.locations.zones.subnets
request_id_field: subnetId
attributes:
- *project
- *location
- *zone
- &subnet
parameter_name: subnetsId
attribute_name: subnet
help: Google Distributed Cloud Edge subnet.
disable_auto_completers: false
interconnect:
name: interconnect
collection: edgenetwork.projects.locations.zones.interconnects
request_id_field: interconnectId
attributes:
- *project
- *location
- *zone
- &interconnect
parameter_name: interconnectsId
attribute_name: interconnect
help: Google Distributed Cloud Edge interconnect.
disable_auto_completers: false
attachment:
name: interconnect attachment
collection: edgenetwork.projects.locations.zones.interconnectAttachments
request_id_field: interconnectAttachmentId
attributes:
- *project
- *location
- *zone
- &interconnectAttachment
parameter_name: interconnectAttachmentsId
attribute_name: interconnect_attachment
help: Google Distributed Cloud Edge interconnect attachment.
disable_auto_completers: false
router:
name: router
collection: edgenetwork.projects.locations.zones.routers
request_id_field: routerId
attributes:
- *project
- *location
- *zone
- &router
parameter_name: routersId
attribute_name: router
help: Google Distributed Cloud Edge router.
disable_auto_completers: false
route:
name: route
collection: edgenetwork.projects.locations.zones.routes
request_id_field: routeId
attributes:
- *project
- *location
- *zone
- &route
parameter_name: routesId
attribute_name: route
help: Google Distributed Cloud Edge route.
disable_auto_completers: false
operation:
name: operation
collection: edgenetwork.projects.locations.operations
attributes:
- *project
- *location
- parameter_name: operationsId
attribute_name: operation
help: Edge-network long running operation.
disable_auto_completers: false

View File

@@ -0,0 +1,162 @@
# -*- coding: utf-8 -*- #
# Copyright 2022 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Flags and helpers for the Distributed Cloud Edge Network routers related commands."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import ipaddress
from googlecloudsdk.api_lib.edge_cloud.networking import utils
from googlecloudsdk.calliope import arg_parsers
def AddDescriptionFlag(parser):
"""Adds a --description flag to the given parser."""
help_text = """Description for the subnet."""
parser.add_argument('--description', help=help_text, required=False)
def AddInterfaceArgs(parser, for_update=False):
"""Adds common arguments for routers add-interface or update-interface."""
help_text = (
"""The argument group for configuring the interface for the router."""
)
operation = 'added'
if for_update:
operation = 'updated'
parser.add_argument(
'--interface-name',
help='The name of the interface being {0}.'.format(operation),
required=True,
)
interface_group = parser.add_argument_group(
mutex=True, help=help_text, required=True
)
southbound_interface_group = interface_group.add_argument_group(
help='The argument group for adding southbound interfaces to edge router.'
)
southbound_interface_group.add_argument(
'--subnetwork',
help='Subnetwork of the interface being {0}.'.format(operation),
)
northbound_interface_group = interface_group.add_argument_group(
help='The argument group for adding northbound interfaces to edge router.'
)
northbound_interface_group.add_argument(
'--interconnect-attachment',
help='Interconnect attachment of the interface being {0}.'.format(
operation
),
)
northbound_interface_group.add_argument(
'--ip-address',
type=utils.IPArgument,
help='Link-local address of the router for this interface.',
)
northbound_interface_group.add_argument(
'--ip-mask-length',
type=arg_parsers.BoundedInt(lower_bound=0, upper_bound=128),
help=(
'Subnet mask for the link-local IP range of the interface. The'
' interface IP address and BGP peer IP address must be selected'
' from the subnet defined by this link-local range.'
),
)
loopback_interface_group = interface_group.add_argument_group(
help='The argument group for adding loopback interfaces to edge router.'
)
loopback_interface_group.add_argument(
'--loopback-ip-addresses',
type=arg_parsers.ArgList(),
metavar='LOOPBACK_IP_ADDRESSES',
help='The list of ip ranges for the loopback interface.',
)
def AddBgpPeerArgs(parser, for_update=False, enable_peer_ipv6_range=False):
"""Adds common arguments for managing BGP peers."""
operation = 'added'
if for_update:
operation = 'updated'
parser.add_argument(
'--interface',
required=not for_update,
help='The name of the interface for this BGP peer.')
parser.add_argument(
'--peer-name',
required=True,
help='The name of the new BGP peer being {0}.'.format(operation))
parser.add_argument(
'--peer-asn',
required=not for_update,
type=int,
help='The BGP autonomous system number (ASN) for this BGP peer. '
'Must be a 16-bit or 32-bit private ASN as defined in '
'https://tools.ietf.org/html/rfc6996, for example `--asn=64512`.')
ip_address_parser = parser.add_mutually_exclusive_group(
required=not for_update
)
ip_address_parser.add_argument(
'--peer-ipv4-range',
help='The IPv4 link-local address range of the peer router.',
)
if enable_peer_ipv6_range:
ip_address_parser.add_argument(
'--peer-ipv6-range',
help='The IPv6 link-local address range of the peer router.',
)
def AddUpdateArgs(parser):
"""Adds arguments for Update."""
def helptext(verb, prep):
return ('{} the comma-separated list of CIDRs {} the set of range '
'advertisements.').format(verb, prep)
def cidrlist(argstr):
split = argstr.split(',')
parsed = map(ipaddress.ip_network, split)
retlist = sorted(parsed)
retset = set(retlist)
if len(retlist) != len(retset):
raise ValueError('CIDR list contained duplicates.')
return retlist
adv_group = parser.add_argument_group(mutex=True)
adv_group.add_argument(
'--add-advertisement-ranges',
help=helptext('add', 'to'),
type=cidrlist,
default=[])
adv_group.add_argument(
'--set-advertisement-ranges',
help=helptext('replace', 'with'),
type=cidrlist,
default=[])
adv_group.add_argument(
'--remove-advertisement-ranges',
help=helptext('remove', 'from'),
type=cidrlist,
default=[])