Skip to content
This repository has been archived by the owner on Nov 29, 2023. It is now read-only.

Commit

Permalink
docs(samples): add usage samples to show handling of LRO response Ope…
Browse files Browse the repository at this point in the history
…ration (#191)

* 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 <gcf-owl-bot[bot]@users.noreply.github.com>
Co-authored-by: Bu Sun Kim <8822365+busunkim96@users.noreply.github.com>
  • Loading branch information
3 people committed Feb 16, 2022
1 parent 807702c commit 309ad62
Show file tree
Hide file tree
Showing 12 changed files with 880 additions and 0 deletions.
13 changes: 13 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,19 @@ Windows
<your-env>\Scripts\activate
<your-env>\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
~~~~~~~~~~
Expand Down
45 changes: 45 additions & 0 deletions samples/snippets/README.md
Original file line number Diff line number Diff line change
@@ -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 <sample_name.py> <arg1> <arg2> ...
```

You can run the following command to find the usage and arguments for the samples:

```python
python <sample_name.py> -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.
111 changes: 111 additions & 0 deletions samples/snippets/create_cluster.py
Original file line number Diff line number Diff line change
@@ -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)
72 changes: 72 additions & 0 deletions samples/snippets/create_cluster_test.py
Original file line number Diff line number Diff line change
@@ -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
104 changes: 104 additions & 0 deletions samples/snippets/delete_cluster.py
Original file line number Diff line number Diff line change
@@ -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)
Loading

0 comments on commit 309ad62

Please sign in to comment.