diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1c590a62647..9c1399cd92a 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -83,6 +83,7 @@ /bigquery-datatransfer/**/* @GoogleCloudPlatform/api-bigquery @GoogleCloudPlatform/python-samples-reviewers @GoogleCloudPlatform/cloud-samples-reviewers /bigquery-migration/**/* @GoogleCloudPlatform/api-bigquery @GoogleCloudPlatform/python-samples-reviewers @GoogleCloudPlatform/cloud-samples-reviewers /bigquery-reservation/**/* @GoogleCloudPlatform/api-bigquery @GoogleCloudPlatform/python-samples-reviewers @GoogleCloudPlatform/cloud-samples-reviewers +/connectgateway/**/* @GoogleCloudPlatform/connectgateway @GoogleCloudPlatform/python-samples-reviewers @GoogleCloudPlatform/cloud-samples-reviewers /dlp/**/* @GoogleCloudPlatform/googleapis-dlp @GoogleCloudPlatform/python-samples-reviewers @GoogleCloudPlatform/cloud-samples-reviewers /functions/spanner/* @GoogleCloudPlatform/api-spanner-python @GoogleCloudPlatform/functions-framework-google @GoogleCloudPlatform/python-samples-reviewers @GoogleCloudPlatform/cloud-samples-reviewers /healthcare/**/* @GoogleCloudPlatform/healthcare-life-sciences @GoogleCloudPlatform/python-samples-reviewers @GoogleCloudPlatform/cloud-samples-reviewers diff --git a/.github/blunderbuss.yml b/.github/blunderbuss.yml index 3df26b635d0..dc137361915 100644 --- a/.github/blunderbuss.yml +++ b/.github/blunderbuss.yml @@ -262,6 +262,10 @@ assign_prs_by: - "api: dataplex" to: - GoogleCloudPlatform/googleapi-dataplex + - labels: + - "api: connectgateway" + to: + - GoogleCloudPlatform/connectgateway # Self-service individuals - labels: - "api: auth" diff --git a/connectgateway/README.md b/connectgateway/README.md new file mode 100644 index 00000000000..a539c1f859f --- /dev/null +++ b/connectgateway/README.md @@ -0,0 +1,10 @@ +# Sample Snippets for Connect Gateway API + +## Quick Start + +In order to run these samples, you first need to go through the following steps: + +1. [Select or create a Cloud Platform project.](https://console.cloud.google.com/project) +2. [Enable billing for your project.](https://cloud.google.com/billing/docs/how-to/modify-project#enable_billing_for_a_project) +3. [Setup Authentication.](https://googleapis.dev/python/google-api-core/latest/auth.html) +4. [Setup Connect Gateway.](https://cloud.google.com/kubernetes-engine/enterprise/multicluster-management/gateway/setup) diff --git a/connectgateway/get_namespace.py b/connectgateway/get_namespace.py new file mode 100644 index 00000000000..ee76853c1f9 --- /dev/null +++ b/connectgateway/get_namespace.py @@ -0,0 +1,97 @@ +# Copyright 2025 Google LLC +# +# 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. + +# [START connectgateway_get_namespace] +import os +import sys + +from google.api_core import exceptions +import google.auth +from google.auth.transport import requests +from google.cloud.gkeconnect import gateway_v1 +from kubernetes import client + + +SCOPES = ['https://www.googleapis.com/auth/cloud-platform'] + + +def get_gateway_url(membership_name: str, location: str) -> str: + """Fetches the GKE Connect Gateway URL for the specified membership.""" + try: + client_options = {} + if location != "global": + # If the location is not global, the endpoint needs to be set to the regional endpoint. + regional_endpoint = f"{location}-connectgateway.googleapis.com" + client_options = {"api_endpoint": regional_endpoint} + gateway_client = gateway_v1.GatewayControlClient(client_options=client_options) + request = gateway_v1.GenerateCredentialsRequest() + request.name = membership_name + response = gateway_client.generate_credentials(request=request) + print(f'GKE Connect Gateway Endpoint: {response.endpoint}') + if not response.endpoint: + print("Error: GKE Connect Gateway Endpoint is empty.") + sys.exit(1) + return response.endpoint + except exceptions.NotFound as e: + print(f'Membership not found: {e}') + sys.exit(1) + except Exception as e: + print(f'Error fetching GKE Connect Gateway URL: {e}') + sys.exit(1) + + +def configure_kubernetes_client(gateway_url: str) -> client.CoreV1Api: + """Configures the Kubernetes client with the GKE Connect Gateway URL and credentials.""" + + configuration = client.Configuration() + + # Configure the API client with the custom host. + configuration.host = gateway_url + + # Configure API key using default auth. + credentials, _ = google.auth.default(scopes=SCOPES) + auth_req = requests.Request() + credentials.refresh(auth_req) + configuration.api_key = {'authorization': f'Bearer {credentials.token}'} + + api_client = client.ApiClient(configuration=configuration) + return client.CoreV1Api(api_client) + + +def get_default_namespace(api_client: client.CoreV1Api) -> None: + """Get default namespace in the Kubernetes cluster.""" + try: + namespace = api_client.read_namespace(name="default") + return namespace + except client.ApiException as e: + print(f"Error getting default namespace: {e}\nStatus: {e.status}\nReason: {e.reason}") + sys.exit(1) + + +def get_namespace(membership_name: str, location: str) -> None: + """Main function to connect to the cluster and get the default namespace.""" + gateway_url = get_gateway_url(membership_name, location) + core_v1_api = configure_kubernetes_client(gateway_url) + namespace = get_default_namespace(core_v1_api) + print(f"\nDefault Namespace:\n{namespace}") + + # [END connectgateway_get_namespace] + + return namespace + + +if __name__ == "__main__": + MEMBERSHIP_NAME = os.environ.get('MEMBERSHIP_NAME') + MEMBERSHIP_LOCATION = os.environ.get("MEMBERSHIP_LOCATION") + namespace = get_namespace(MEMBERSHIP_NAME, MEMBERSHIP_LOCATION) diff --git a/connectgateway/get_namespace_test.py b/connectgateway/get_namespace_test.py new file mode 100644 index 00000000000..95445989f38 --- /dev/null +++ b/connectgateway/get_namespace_test.py @@ -0,0 +1,89 @@ +# Copyright 2025 Google LLC +# +# 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. + +import os +from time import sleep +import uuid + + +from google.cloud import container_v1 as gke + +import pytest + +import get_namespace + +PROJECT_ID = os.environ["GOOGLE_CLOUD_PROJECT"] +ZONE = "us-central1-a" +REGION = "us-central1" +CLUSTER_NAME = f"cluster-{uuid.uuid4().hex[:10]}" + + +@pytest.fixture(autouse=True) +def setup_and_tear_down() -> None: + create_cluster(PROJECT_ID, ZONE, CLUSTER_NAME) + + yield + + delete_cluster(PROJECT_ID, ZONE, CLUSTER_NAME) + + +def poll_operation(client: gke.ClusterManagerClient, op_id: str) -> None: + + while True: + # Make GetOperation request + operation = client.get_operation({"name": op_id}) + # Print the Operation Information + print(operation) + + # Stop polling when Operation is done. + if operation.status == gke.Operation.Status.DONE: + break + + # Wait 30 seconds before polling again + sleep(30) + + +def create_cluster(project_id: str, location: str, cluster_name: str) -> None: + """Create a new GKE cluster in the given GCP Project and Zone/Region.""" + # Initialize the Cluster management client. + client = gke.ClusterManagerClient() + cluster_location = client.common_location_path(project_id, location) + cluster_def = { + "name": str(cluster_name), + "initial_node_count": 1, + "fleet": {"project": str(project_id)}, + } + + # Create the request object with the location identifier. + request = {"parent": cluster_location, "cluster": cluster_def} + create_response = client.create_cluster(request) + op_identifier = f"{cluster_location}/operations/{create_response.name}" + # poll for the operation status and schedule a retry until the cluster is created + poll_operation(client, op_identifier) + + +def delete_cluster(project_id: str, location: str, cluster_name: str) -> None: + """Delete the created GKE cluster.""" + client = gke.ClusterManagerClient() + cluster_location = client.common_location_path(project_id, location) + cluster_name = f"{cluster_location}/clusters/{cluster_name}" + client.delete_cluster({"name": cluster_name}) + + +def test_get_namespace() -> None: + membership_name = f"projects/{PROJECT_ID}/locations/{REGION}/memberships/{CLUSTER_NAME}" + results = get_namespace.get_namespace(membership_name, REGION) + + assert results is not None + assert results.metadata.name == "default" diff --git a/connectgateway/noxfile_config.py b/connectgateway/noxfile_config.py new file mode 100644 index 00000000000..ea71c27ca40 --- /dev/null +++ b/connectgateway/noxfile_config.py @@ -0,0 +1,22 @@ +# Copyright 2025 Google LLC +# +# 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. + +TEST_CONFIG_OVERRIDE = { + # You can opt out from the test for specific Python versions. + "ignored_versions": ["2.7", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12"], + "enforce_type_hints": True, + "gcloud_project_env": "GOOGLE_CLOUD_PROJECT", + "pip_version_override": None, + "envs": {}, +} diff --git a/connectgateway/requirements-test.txt b/connectgateway/requirements-test.txt new file mode 100644 index 00000000000..8c22c500206 --- /dev/null +++ b/connectgateway/requirements-test.txt @@ -0,0 +1,2 @@ +google-cloud-container==2.56.1 +pytest==8.3.5 \ No newline at end of file diff --git a/connectgateway/requirements.txt b/connectgateway/requirements.txt new file mode 100644 index 00000000000..27496fb1cf9 --- /dev/null +++ b/connectgateway/requirements.txt @@ -0,0 +1,4 @@ +google-cloud-gke-connect-gateway==0.10.3 +google-auth==2.38.0 +kubernetes==32.0.1 +google-api-core==2.24.2