From fc414c230169a5caa0290387a82a0df88069e433 Mon Sep 17 00:00:00 2001 From: Shabir Mohamed Abdul Samadh <7249208+Shabirmean@users.noreply.github.com> Date: Tue, 15 Feb 2022 20:36:05 -0500 Subject: [PATCH] docs(samples): add usage samples to show handling of LRO response Operation (#191) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * improvement: add samples for the using the library * cleanup: update the git ignore * 🦉 Updates from OwlBot See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * chore: remove unnecessary comments * chore: update git ignore * 🦉 Updates from OwlBot See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * doc: add simple usage instructions to README * cleanup: add copyright headers * chore: update git ignore * 🦉 Updates from OwlBot See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * doc: add more details to retry function doc * chore: update git ignore * 🦉 Updates from OwlBot See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * doc: add seperate sections about the samples * chore: update git ignore * 🦉 Updates from OwlBot See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * cleanup: add region tags * doc: update the readme to remove inline code * chore: update git ignore * 🦉 Updates from OwlBot See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * doc: update readme * chore: update git ignore * 🦉 Updates from OwlBot See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * process: add nox and fix lint errors * chore: update git ignore * 🦉 Updates from OwlBot See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * process: add requirements.txt * chore: add lro samples to gitignore * test: add test for quickstart * test: add test for create cluster * test: add test for delete cluster * test: fix fixture in test * 🦉 Updates from OwlBot See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * lint: finx linting errors * cleanup: remove the main from being inside the region tags * cleanup: remove the main from being inside the region tags * cleanup: remove the main from being inside the region tags * 🦉 Updates from OwlBot See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * 🦉 Updates from OwlBot See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * 🦉 Updates from OwlBot See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * doc: fix typo Co-authored-by: Bu Sun Kim <8822365+busunkim96@users.noreply.github.com> * doc: pr comment doc update Co-authored-by: Bu Sun Kim <8822365+busunkim96@users.noreply.github.com> * doc: add license headers to missing files * 🦉 Updates from OwlBot See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md Co-authored-by: Owl Bot Co-authored-by: Bu Sun Kim <8822365+busunkim96@users.noreply.github.com> --- packages/google-cloud-container/README.rst | 13 + .../samples/snippets/README.md | 45 +++ .../samples/snippets/create_cluster.py | 111 +++++++ .../samples/snippets/create_cluster_test.py | 72 +++++ .../samples/snippets/delete_cluster.py | 104 +++++++ .../samples/snippets/delete_cluster_test.py | 96 ++++++ .../samples/snippets/noxfile.py | 279 ++++++++++++++++++ .../samples/snippets/noxfile_config.py | 42 +++ .../samples/snippets/quickstart.py | 56 ++++ .../samples/snippets/quickstart_test.py | 55 ++++ .../samples/snippets/requirement-test.txt | 4 + .../samples/snippets/requirements.txt | 3 + 12 files changed, 880 insertions(+) create mode 100644 packages/google-cloud-container/samples/snippets/README.md create mode 100644 packages/google-cloud-container/samples/snippets/create_cluster.py create mode 100644 packages/google-cloud-container/samples/snippets/create_cluster_test.py create mode 100644 packages/google-cloud-container/samples/snippets/delete_cluster.py create mode 100644 packages/google-cloud-container/samples/snippets/delete_cluster_test.py create mode 100644 packages/google-cloud-container/samples/snippets/noxfile.py create mode 100644 packages/google-cloud-container/samples/snippets/noxfile_config.py create mode 100644 packages/google-cloud-container/samples/snippets/quickstart.py create mode 100644 packages/google-cloud-container/samples/snippets/quickstart_test.py create mode 100644 packages/google-cloud-container/samples/snippets/requirement-test.txt create mode 100644 packages/google-cloud-container/samples/snippets/requirements.txt diff --git a/packages/google-cloud-container/README.rst b/packages/google-cloud-container/README.rst index 22b2d807fa09..ca0b09115bb6 100644 --- a/packages/google-cloud-container/README.rst +++ b/packages/google-cloud-container/README.rst @@ -79,6 +79,19 @@ Windows \Scripts\activate \Scripts\pip.exe install google-cloud-container +Once you have the virtual environment setup and activated, you can install the library: + +.. code-block:: console + + pip install google-cloud-container + +Using the client library +~~~~~~~~~~~~~~~~~~~~~~~~ + +See the examples in the `samples`_ directory. You can start with `quickstart.py`_. + +.. _samples: /samples +.. _quickstart.py: /samples/snippets/quickstart.py Next Steps ~~~~~~~~~~ diff --git a/packages/google-cloud-container/samples/snippets/README.md b/packages/google-cloud-container/samples/snippets/README.md new file mode 100644 index 000000000000..a1d0691fd8c2 --- /dev/null +++ b/packages/google-cloud-container/samples/snippets/README.md @@ -0,0 +1,45 @@ +# Samples + +All the samples are self contained unless they are placed inside their own folders. The samples use [Application Default Credentails (ADC)](https://cloud.google.com/docs/authentication/production#automatically) to authenticate with GCP. So make sure ADC is setup correctly _(i.e. `GOOGLE_APPLICATION_CREDENTIALS` environment variable is set)_ before running the samples. Some sample might require additional python modules to be installed. + +You can run samples as follows: + +```python +python ... +``` + +You can run the following command to find the usage and arguments for the samples: + +```python +python -h +``` +```bash +# example +python quickstart.py -h + +usage: quickstart.py [-h] project_id zone + +positional arguments: + project_id Google Cloud project ID + zone GKE Cluster zone + +optional arguments: + -h, --help show this help message and exit +``` + +### Quickstart sample +- [**quickstart.py**](quickstart.py): A simple example to list the GKE clusters in a given GCP project and zone. The sample uses the [`list_clusters()`](https://cloud.google.com/python/docs/reference/container/latest/google.cloud.container_v1.services.cluster_manager.ClusterManagerClient#google_cloud_container_v1_services_cluster_manager_ClusterManagerClient_list_clusters) API to fetch the list of cluster. + + +### Long running operation sample + +The following samples are examples of operations that take a while to complete. +For example _creating a cluster_ in GKE can take a while to set up the cluster +nodes, networking and configuring Kubernetes. Thus, calls to such long running +APIs return an object of type [`Operation`](https://cloud.google.com/python/docs/reference/container/latest/google.cloud.container_v1.types.Operation). We can +then use the id of the returned operation to **poll** the [`get_operation()`](https://cloud.google.com/python/docs/reference/container/latest/google.cloud.container_v1.services.cluster_manager.ClusterManagerClient#google_cloud_container_v1_services_cluster_manager_ClusterManagerClient_get_operation) API to check for it's status. You can see the +different statuses it can be in, in [this proto definition](https://github.com/googleapis/googleapis/blob/master/google/container/v1/cluster_service.proto#L1763-L1778). + +- [**create_cluster.py**](create_cluster.py): An example of creating a GKE cluster _(with mostly the defaults)_. This example shows how to handle responses of type [`Operation`](https://cloud.google.com/python/docs/reference/container/latest/google.cloud.container_v1.types.Operation) that reperesents a long running operation. The example uses the python module [`backoff`](https://github.com/litl/backoff) to handle a graceful exponential backoff retry mechanism to check if the `Operation` has completed. + +- [**delete_cluster.py**](delete_cluster.py): An example of deleting a GKE cluster. This example shows how to handle responses of type [`Operation`](https://cloud.google.com/python/docs/reference/container/latest/google.cloud.container_v1.types.Operation) that reperesents a long running operation. \ No newline at end of file diff --git a/packages/google-cloud-container/samples/snippets/create_cluster.py b/packages/google-cloud-container/samples/snippets/create_cluster.py new file mode 100644 index 000000000000..ad5ddfc39981 --- /dev/null +++ b/packages/google-cloud-container/samples/snippets/create_cluster.py @@ -0,0 +1,111 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 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 gke_create_cluster] +import argparse +import sys +from typing import Dict + +import backoff +from google.cloud import container_v1 + + +def on_success(details: Dict[str, str]) -> None: + """ + A handler function to pass into the retry backoff algorithm as the function + to be executed upon a successful attempt. + + Read the `Event handlers` section of the backoff python module at: + https://pypi.org/project/backoff/ + """ + print("Successfully created cluster after {elapsed:0.1f} seconds".format(**details)) + + +def on_failure(details: Dict[str, str]) -> None: + """ + A handler function to pass into the retry backoff algorithm as the function + to be executed upon a failed attempt. + + Read the `Event handlers` section of the backoff python module at: + https://pypi.org/project/backoff/ + """ + print("Backing off {wait:0.1f} seconds after {tries} tries".format(**details)) + + +@backoff.on_predicate( + # the backoff algorithm to use. we use exponential backoff here + backoff.expo, + # the test function on the return value to determine if a retry is necessary + lambda x: x != container_v1.Operation.Status.DONE, + # maximum number of times to retry before giving up + max_tries=20, + # function to execute upon a failure and when a retry a scheduled + on_backoff=on_failure, + # function to execute upon a successful attempt and no more retries needed + on_success=on_success, +) +def poll_for_op_status( + client: container_v1.ClusterManagerClient, op_id: str +) -> container_v1.Operation.Status: + """ + This function calls the Operation API in GCP with the given operation id. It + serves as a simple retry function that fetches the operation and returns + it's status. + + We use the 'backoff' python module to provide us the implementation of the + backoff & retry strategy. The function is annotated with the `backoff` + python module to schedule this function based on a reasonable backoff + algorithm. + """ + + op = client.get_operation({"name": op_id}) + return op.status + + +def create_cluster(project_id: str, location: str, cluster_name: str) -> None: + """Create a new GKE cluster in the given GCP Project and Zone""" + # Initialize the Cluster management client. + client = container_v1.ClusterManagerClient() + # Create a fully qualified location identifier of form `projects/{project_id}/location/{zone}'. + cluster_location = client.common_location_path(project_id, location) + cluster_def = { + "name": cluster_name, + "initial_node_count": 2, + "node_config": {"machine_type": "e2-standard-2"}, + } + # 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_for_op_status(client, op_identifier) + + +# [END gke_create_cluster] + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument("project_id", help="Google Cloud project ID") + parser.add_argument("zone", help="GKE Cluster zone") + parser.add_argument("cluster_name", help="Name to be given to the GKE Cluster") + args = parser.parse_args() + + if len(sys.argv) != 4: + parser.print_usage() + sys.exit(1) + + create_cluster(args.project_id, args.zone, args.cluster_name) diff --git a/packages/google-cloud-container/samples/snippets/create_cluster_test.py b/packages/google-cloud-container/samples/snippets/create_cluster_test.py new file mode 100644 index 000000000000..a06ab2c511d2 --- /dev/null +++ b/packages/google-cloud-container/samples/snippets/create_cluster_test.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 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 +import uuid + +import backoff + +from google.cloud import container_v1 as gke + +import pytest + +import create_cluster as gke_create + +PROJECT_ID = os.environ["GOOGLE_CLOUD_PROJECT"] +ZONE = "us-central1-b" +CLUSTER_NAME = f"py-container-repo-test-{uuid.uuid4().hex[:10]}" + + +@pytest.fixture(autouse=True) +def setup_and_tear_down() -> None: + + # nohing to setup here + + # run the tests here + yield + + # delete the cluster + client = gke.ClusterManagerClient() + cluster_location = client.common_location_path(PROJECT_ID, ZONE) + cluster_name = f"{cluster_location}/clusters/{CLUSTER_NAME}" + op = client.delete_cluster({"name": cluster_name}) + op_id = f"{cluster_location}/operations/{op.name}" + + # schedule a retry to ensure the cluster is deleted + @backoff.on_predicate( + backoff.expo, lambda x: x != gke.Operation.Status.DONE, max_tries=20 + ) + def wait_for_delete() -> gke.Operation.Status: + return client.get_operation({"name": op_id}).status + + wait_for_delete() + + +def test_create_clusters(capsys: object) -> None: + gke_create.create_cluster(PROJECT_ID, ZONE, CLUSTER_NAME) + out, _ = capsys.readouterr() + + assert "Backing off " in out + assert "Successfully created cluster after" in out + + client = gke.ClusterManagerClient() + cluster_location = client.common_location_path(PROJECT_ID, ZONE) + list_response = client.list_clusters({"parent": cluster_location}) + + list_of_clusters = [] + for cluster in list_response.clusters: + list_of_clusters.append(cluster.name) + + assert CLUSTER_NAME in list_of_clusters diff --git a/packages/google-cloud-container/samples/snippets/delete_cluster.py b/packages/google-cloud-container/samples/snippets/delete_cluster.py new file mode 100644 index 000000000000..0290766e9b98 --- /dev/null +++ b/packages/google-cloud-container/samples/snippets/delete_cluster.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 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 gke_delete_cluster] +import argparse +import sys +from typing import Dict + +import backoff +from google.cloud import container_v1 + + +def on_success(details: Dict[str, str]) -> None: + """ + A handler function to pass into the retry backoff algorithm as the function + to be executed upon a successful attempt. + + Read the `Event handlers` section of the backoff python module at: + https://pypi.org/project/backoff/ + """ + print("Successfully deleted cluster after {elapsed:0.1f} seconds".format(**details)) + + +def on_failure(details: Dict[str, str]) -> None: + """ + A handler function to pass into the retry backoff algorithm as the function + to be executed upon a failed attempt. + + Read the `Event handlers` section of the backoff python module at: + https://pypi.org/project/backoff/ + """ + print("Backing off {wait:0.1f} seconds after {tries} tries".format(**details)) + + +@backoff.on_predicate( + # the backoff algorithm to use. we use exponential backoff here + backoff.expo, + # the test function on the return value to determine if a retry is necessary + lambda x: x != container_v1.Operation.Status.DONE, + # maximum number of times to retry before giving up + max_tries=20, + # function to execute upon a failure and when a retry is scheduled + on_backoff=on_failure, + # function to execute upon a successful attempt and no more retries needed + on_success=on_success, +) +def poll_for_op_status( + client: container_v1.ClusterManagerClient, op_id: str +) -> container_v1.Operation.Status: + """ + A simple retry function that fetches the operation and returns it's status. + + The function is annotated with the `backoff` python module to schedule this + function based on a reasonable backoff algorithm + """ + + op = client.get_operation({"name": op_id}) + return op.status + + +def delete_cluster(project_id: str, location: str, cluster_name: str) -> None: + """Delete an existing GKE cluster in the given GCP Project and Zone""" + + # Initialize the Cluster management client. + client = container_v1.ClusterManagerClient() + # Create a fully qualified location identifier of form `projects/{project_id}/location/{zone}'. + cluster_location = client.common_location_path(project_id, location) + cluster_name = f"{cluster_location}/clusters/{cluster_name}" + # Create the request object with the location identifier. + request = {"name": cluster_name} + delete_response = client.delete_cluster(request) + op_identifier = f"{cluster_location}/operations/{delete_response.name}" + # poll for the operation status until the cluster is deleted + poll_for_op_status(client, op_identifier) + + +# [END gke_delete_cluster] + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument("project_id", help="Google Cloud project ID") + parser.add_argument("zone", help="GKE Cluster zone") + parser.add_argument("cluster_name", help="Name to be given to the GKE Cluster") + args = parser.parse_args() + + if len(sys.argv) != 4: + parser.print_usage() + sys.exit(1) + + delete_cluster(args.project_id, args.zone, args.cluster_name) diff --git a/packages/google-cloud-container/samples/snippets/delete_cluster_test.py b/packages/google-cloud-container/samples/snippets/delete_cluster_test.py new file mode 100644 index 000000000000..fc7845d3465e --- /dev/null +++ b/packages/google-cloud-container/samples/snippets/delete_cluster_test.py @@ -0,0 +1,96 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 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 +import uuid + +import backoff + +from google.api_core import exceptions as googleEx +from google.cloud import container_v1 as gke + +import pytest + +import delete_cluster as gke_delete + +PROJECT_ID = os.environ["GOOGLE_CLOUD_PROJECT"] +ZONE = "us-central1-b" +CLUSTER_NAME = f"py-container-repo-test-{uuid.uuid4().hex[:10]}" + + +@pytest.fixture(autouse=True) +def setup_and_tear_down() -> None: + + # create a cluster to be deleted + client = gke.ClusterManagerClient() + cluster_location = client.common_location_path(PROJECT_ID, ZONE) + cluster_def = { + "name": CLUSTER_NAME, + "initial_node_count": 2, + "node_config": {"machine_type": "e2-standard-2"}, + } + op = client.create_cluster({"parent": cluster_location, "cluster": cluster_def}) + op_id = f"{cluster_location}/operations/{op.name}" + + # schedule a retry to ensure the cluster is created + @backoff.on_predicate( + backoff.expo, lambda x: x != gke.Operation.Status.DONE, max_tries=20 + ) + def wait_for_create() -> gke.Operation.Status: + return client.get_operation({"name": op_id}).status + + wait_for_create() + + # run the tests here + yield + + # delete the cluster in case the test itself failed + client = gke.ClusterManagerClient() + cluster_location = client.common_location_path(PROJECT_ID, ZONE) + cluster_name = f"{cluster_location}/clusters/{CLUSTER_NAME}" + + try: + op = client.delete_cluster({"name": cluster_name}) + op_id = f"{cluster_location}/operations/{op.name}" + + # schedule a retry to ensure the cluster is deleted + @backoff.on_predicate( + backoff.expo, lambda x: x != gke.Operation.Status.DONE, max_tries=20 + ) + def wait_for_delete() -> gke.Operation.Status: + return client.get_operation({"name": op_id}).status + + wait_for_delete() + except googleEx.NotFound: + # if the delete test passed then this is bound to happen + pass + + +def test_delete_clusters(capsys: object) -> None: + gke_delete.delete_cluster(PROJECT_ID, ZONE, CLUSTER_NAME) + out, _ = capsys.readouterr() + + assert "Backing off " in out + assert "Successfully deleted cluster after" in out + + client = gke.ClusterManagerClient() + cluster_location = client.common_location_path(PROJECT_ID, ZONE) + list_response = client.list_clusters({"parent": cluster_location}) + + list_of_clusters = [] + for cluster in list_response.clusters: + list_of_clusters.append(cluster.name) + + assert CLUSTER_NAME not in list_of_clusters diff --git a/packages/google-cloud-container/samples/snippets/noxfile.py b/packages/google-cloud-container/samples/snippets/noxfile.py new file mode 100644 index 000000000000..20cdfc620138 --- /dev/null +++ b/packages/google-cloud-container/samples/snippets/noxfile.py @@ -0,0 +1,279 @@ +# Copyright 2019 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. + +from __future__ import print_function + +import glob +import os +from pathlib import Path +import sys +from typing import Callable, Dict, List, Optional + +import nox + + +# WARNING - WARNING - WARNING - WARNING - WARNING +# WARNING - WARNING - WARNING - WARNING - WARNING +# DO NOT EDIT THIS FILE EVER! +# WARNING - WARNING - WARNING - WARNING - WARNING +# WARNING - WARNING - WARNING - WARNING - WARNING + +BLACK_VERSION = "black==19.10b0" + +# Copy `noxfile_config.py` to your directory and modify it instead. + +# `TEST_CONFIG` dict is a configuration hook that allows users to +# modify the test configurations. The values here should be in sync +# with `noxfile_config.py`. Users will copy `noxfile_config.py` into +# their directory and modify it. + +TEST_CONFIG = { + # You can opt out from the test for specific Python versions. + "ignored_versions": [], + # Old samples are opted out of enforcing Python type hints + # All new samples should feature them + "enforce_type_hints": False, + # An envvar key for determining the project id to use. Change it + # to 'BUILD_SPECIFIC_GCLOUD_PROJECT' if you want to opt in using a + # build specific Cloud project. You can also use your own string + # to use your own Cloud project. + "gcloud_project_env": "GOOGLE_CLOUD_PROJECT", + # 'gcloud_project_env': 'BUILD_SPECIFIC_GCLOUD_PROJECT', + # If you need to use a specific version of pip, + # change pip_version_override to the string representation + # of the version number, for example, "20.2.4" + "pip_version_override": None, + # A dictionary you want to inject into your test. Don't put any + # secrets here. These values will override predefined values. + "envs": {}, +} + + +try: + # Ensure we can import noxfile_config in the project's directory. + sys.path.append(".") + from noxfile_config import TEST_CONFIG_OVERRIDE +except ImportError as e: + print("No user noxfile_config found: detail: {}".format(e)) + TEST_CONFIG_OVERRIDE = {} + +# Update the TEST_CONFIG with the user supplied values. +TEST_CONFIG.update(TEST_CONFIG_OVERRIDE) + + +def get_pytest_env_vars() -> Dict[str, str]: + """Returns a dict for pytest invocation.""" + ret = {} + + # Override the GCLOUD_PROJECT and the alias. + env_key = TEST_CONFIG["gcloud_project_env"] + # This should error out if not set. + ret["GOOGLE_CLOUD_PROJECT"] = os.environ[env_key] + + # Apply user supplied envs. + ret.update(TEST_CONFIG["envs"]) + return ret + + +# DO NOT EDIT - automatically generated. +# All versions used to test samples. +ALL_VERSIONS = ["3.6", "3.7", "3.8", "3.9", "3.10"] + +# Any default versions that should be ignored. +IGNORED_VERSIONS = TEST_CONFIG["ignored_versions"] + +TESTED_VERSIONS = sorted([v for v in ALL_VERSIONS if v not in IGNORED_VERSIONS]) + +INSTALL_LIBRARY_FROM_SOURCE = os.environ.get("INSTALL_LIBRARY_FROM_SOURCE", False) in ( + "True", + "true", +) + +# Error if a python version is missing +nox.options.error_on_missing_interpreters = True + +# +# Style Checks +# + + +def _determine_local_import_names(start_dir: str) -> List[str]: + """Determines all import names that should be considered "local". + + This is used when running the linter to insure that import order is + properly checked. + """ + file_ext_pairs = [os.path.splitext(path) for path in os.listdir(start_dir)] + return [ + basename + for basename, extension in file_ext_pairs + if extension == ".py" + or os.path.isdir(os.path.join(start_dir, basename)) + and basename not in ("__pycache__") + ] + + +# Linting with flake8. +# +# We ignore the following rules: +# E203: whitespace before ‘:’ +# E266: too many leading ‘#’ for block comment +# E501: line too long +# I202: Additional newline in a section of imports +# +# We also need to specify the rules which are ignored by default: +# ['E226', 'W504', 'E126', 'E123', 'W503', 'E24', 'E704', 'E121'] +FLAKE8_COMMON_ARGS = [ + "--show-source", + "--builtin=gettext", + "--max-complexity=20", + "--import-order-style=google", + "--exclude=.nox,.cache,env,lib,generated_pb2,*_pb2.py,*_pb2_grpc.py", + "--ignore=E121,E123,E126,E203,E226,E24,E266,E501,E704,W503,W504,I202", + "--max-line-length=88", +] + + +@nox.session +def lint(session: nox.sessions.Session) -> None: + if not TEST_CONFIG["enforce_type_hints"]: + session.install("flake8", "flake8-import-order") + else: + session.install("flake8", "flake8-import-order", "flake8-annotations") + + local_names = _determine_local_import_names(".") + args = FLAKE8_COMMON_ARGS + [ + "--application-import-names", + ",".join(local_names), + ".", + ] + session.run("flake8", *args) + + +# +# Black +# + + +@nox.session +def blacken(session: nox.sessions.Session) -> None: + session.install(BLACK_VERSION) + python_files = [path for path in os.listdir(".") if path.endswith(".py")] + + session.run("black", *python_files) + + +# +# Sample Tests +# + + +PYTEST_COMMON_ARGS = ["--junitxml=sponge_log.xml"] + + +def _session_tests( + session: nox.sessions.Session, post_install: Callable = None +) -> None: + # check for presence of tests + test_list = glob.glob("*_test.py") + glob.glob("test_*.py") + test_list.extend(glob.glob("tests")) + if len(test_list) == 0: + print("No tests found, skipping directory.") + else: + if TEST_CONFIG["pip_version_override"]: + pip_version = TEST_CONFIG["pip_version_override"] + session.install(f"pip=={pip_version}") + """Runs py.test for a particular project.""" + if os.path.exists("requirements.txt"): + if os.path.exists("constraints.txt"): + session.install("-r", "requirements.txt", "-c", "constraints.txt") + else: + session.install("-r", "requirements.txt") + + if os.path.exists("requirements-test.txt"): + if os.path.exists("constraints-test.txt"): + session.install( + "-r", "requirements-test.txt", "-c", "constraints-test.txt" + ) + else: + session.install("-r", "requirements-test.txt") + + if INSTALL_LIBRARY_FROM_SOURCE: + session.install("-e", _get_repo_root()) + + if post_install: + post_install(session) + + session.run( + "pytest", + *(PYTEST_COMMON_ARGS + session.posargs), + # Pytest will return 5 when no tests are collected. This can happen + # on travis where slow and flaky tests are excluded. + # See http://doc.pytest.org/en/latest/_modules/_pytest/main.html + success_codes=[0, 5], + env=get_pytest_env_vars(), + ) + + +@nox.session(python=ALL_VERSIONS) +def py(session: nox.sessions.Session) -> None: + """Runs py.test for a sample using the specified version of Python.""" + if session.python in TESTED_VERSIONS: + _session_tests(session) + else: + session.skip( + "SKIPPED: {} tests are disabled for this sample.".format(session.python) + ) + + +# +# Readmegen +# + + +def _get_repo_root() -> Optional[str]: + """ Returns the root folder of the project. """ + # Get root of this repository. Assume we don't have directories nested deeper than 10 items. + p = Path(os.getcwd()) + for i in range(10): + if p is None: + break + if Path(p / ".git").exists(): + return str(p) + # .git is not available in repos cloned via Cloud Build + # setup.py is always in the library's root, so use that instead + # https://github.com/googleapis/synthtool/issues/792 + if Path(p / "setup.py").exists(): + return str(p) + p = p.parent + raise Exception("Unable to detect repository root.") + + +GENERATED_READMES = sorted([x for x in Path(".").rglob("*.rst.in")]) + + +@nox.session +@nox.parametrize("path", GENERATED_READMES) +def readmegen(session: nox.sessions.Session, path: str) -> None: + """(Re-)generates the readme for a sample.""" + session.install("jinja2", "pyyaml") + dir_ = os.path.dirname(path) + + if os.path.exists(os.path.join(dir_, "requirements.txt")): + session.install("-r", os.path.join(dir_, "requirements.txt")) + + in_file = os.path.join(dir_, "README.rst.in") + session.run( + "python", _get_repo_root() + "/scripts/readme-gen/readme_gen.py", in_file + ) diff --git a/packages/google-cloud-container/samples/snippets/noxfile_config.py b/packages/google-cloud-container/samples/snippets/noxfile_config.py new file mode 100644 index 000000000000..16f326e5c6d2 --- /dev/null +++ b/packages/google-cloud-container/samples/snippets/noxfile_config.py @@ -0,0 +1,42 @@ +# Copyright 2022 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. + +# Default TEST_CONFIG_OVERRIDE for python repos. + +# You can copy this file into your directory, then it will be imported from +# the noxfile.py. + +# The source of truth: +# https://github.com/GoogleCloudPlatform/python-docs-samples/blob/main/noxfile_config.py + +TEST_CONFIG_OVERRIDE = { + # You can opt out from the test for specific Python versions. + "ignored_versions": ["2.7"], + # Old samples are opted out of enforcing Python type hints + # All new samples should feature them + "enforce_type_hints": True, + # An envvar key for determining the project id to use. Change it + # to 'BUILD_SPECIFIC_GCLOUD_PROJECT' if you want to opt in using a + # build specific Cloud project. You can also use your own string + # to use your own Cloud project. + "gcloud_project_env": "GOOGLE_CLOUD_PROJECT", + # 'gcloud_project_env': 'BUILD_SPECIFIC_GCLOUD_PROJECT', + # If you need to use a specific version of pip, + # change pip_version_override to the string representation + # of the version number, for example, "20.2.4" + "pip_version_override": None, + # A dictionary you want to inject into your test. Don't put any + # secrets here. These values will override predefined values. + "envs": {}, +} diff --git a/packages/google-cloud-container/samples/snippets/quickstart.py b/packages/google-cloud-container/samples/snippets/quickstart.py new file mode 100644 index 000000000000..b7cecb52e05e --- /dev/null +++ b/packages/google-cloud-container/samples/snippets/quickstart.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 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 gke_list_cluster] +import argparse +import sys + +from google.cloud import container_v1 + + +def list_clusters(project_id: str, location: str) -> None: + """List all the GKE clusters in the given GCP Project and Zone""" + + # Initialize the Cluster management client. + client = container_v1.ClusterManagerClient() + # Create a fully qualified location identifier of form `projects/{project_id}/location/{zone}'. + cluster_location = client.common_location_path(project_id, location) + # Create the request object with the location identifier. + request = {"parent": cluster_location} + list_response = client.list_clusters(request) + + print( + f"There were {len(list_response.clusters)} clusters in {location} for project {project_id}." + ) + for cluster in list_response.clusters: + print(f"- {cluster.name}") + + +# [END gke_list_cluster] + +if __name__ == "__main__": + + parser = argparse.ArgumentParser( + description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument("project_id", help="Google Cloud project ID") + parser.add_argument("zone", help="GKE Cluster zone") + args = parser.parse_args() + + if len(sys.argv) != 3: + parser.print_usage() + sys.exit(1) + + list_clusters(args.project_id, args.zone) diff --git a/packages/google-cloud-container/samples/snippets/quickstart_test.py b/packages/google-cloud-container/samples/snippets/quickstart_test.py new file mode 100644 index 000000000000..7904fa5535c6 --- /dev/null +++ b/packages/google-cloud-container/samples/snippets/quickstart_test.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 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 + +import quickstart as gke_list + +PROJECT_ID = os.environ["GOOGLE_CLOUD_PROJECT"] +ZONE = "us-central1-b" + + +def test_list_clusters(capsys: object) -> None: + output_prefix = "There were " + output_suffix = f" clusters in {ZONE} for project {PROJECT_ID}." + + gke_list.list_clusters(PROJECT_ID, ZONE) + out, _ = capsys.readouterr() + + """ + Typical output looks as follows: + + There were 3 clusters in us-central1-b for project test-project. + - cluster1 + - cluster2 + - cluster3 + + Split array by '\n' + [ + "There were 3 clusters in us-central1-b for project test-project.", + "- cluster1", + "- cluster2", + "- cluster3", + "", + ] + """ + out_lines = out.split("\n") + first_line = out_lines[0] + first_line = first_line.replace(output_prefix, "") + first_line = first_line.replace(output_suffix, "") + cluster_count = int(first_line) # get the cluster count in the first line + + assert output_suffix in out + assert cluster_count == len(out_lines) - 2 diff --git a/packages/google-cloud-container/samples/snippets/requirement-test.txt b/packages/google-cloud-container/samples/snippets/requirement-test.txt new file mode 100644 index 000000000000..2336a1237f24 --- /dev/null +++ b/packages/google-cloud-container/samples/snippets/requirement-test.txt @@ -0,0 +1,4 @@ + +backoff==1.11.1 +pytest==7.0.0 +google-api-core=2.5.0 \ No newline at end of file diff --git a/packages/google-cloud-container/samples/snippets/requirements.txt b/packages/google-cloud-container/samples/snippets/requirements.txt new file mode 100644 index 000000000000..748ddb991437 --- /dev/null +++ b/packages/google-cloud-container/samples/snippets/requirements.txt @@ -0,0 +1,3 @@ +google-cloud-container==2.10.1 +backoff==1.11.1 +pytest==7.0.0 \ No newline at end of file