├── .github
├── renovate.json
├── snippet-bot.yml
└── sync-repo-settings.yaml
├── .gitignore
├── .kokoro
├── common.cfg
├── docker
│ └── Dockerfile
├── presubmit.cfg
├── system_tests.cfg
├── system_tests.sh
├── trampoline.sh
└── trampoline_v2.sh
├── .trampolinerc
├── CODEOWNERS
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── authenticating-users
├── app.yaml
├── main.py
├── main_test.py
├── requirements-test.txt
└── requirements.txt
├── background
├── README.md
├── app
│ ├── app.yaml
│ ├── main.py
│ ├── main_test.py
│ ├── requirements.txt
│ └── templates
│ │ └── index.html
└── function
│ ├── main.py
│ ├── main_test.py
│ └── requirements.txt
├── bookshelf
├── Dockerfile
├── app.yaml
├── firestore.py
├── images
│ └── moby-dick.png
├── main.py
├── main_test.py
├── requirements.txt
├── storage.py
└── templates
│ ├── base.html
│ ├── form.html
│ ├── list.html
│ └── view.html
├── decrypt-secrets.sh
├── encrypt-secrets.sh
├── gce
├── README.md
├── add-google-cloud-ops-agent-repo.sh
├── deploy.sh
├── main.py
├── main_test.py
├── procfile
├── python-app.conf
├── requirements.txt
├── startup-script.sh
└── teardown.sh
├── noxfile.py
├── optional-kubernetes-engine
├── .dockerignore
├── Dockerfile
├── Makefile
├── README.md
├── bookshelf-frontend.yaml
├── bookshelf-service.yaml
├── bookshelf-worker.yaml
├── bookshelf
│ ├── __init__.py
│ ├── crud.py
│ ├── model_cloudsql.py
│ ├── model_datastore.py
│ ├── model_mongodb.py
│ ├── storage.py
│ ├── tasks.py
│ └── templates
│ │ ├── base.html
│ │ ├── form.html
│ │ ├── list.html
│ │ └── view.html
├── config.py
├── main.py
├── procfile
├── requirements-dev.txt
├── requirements.txt
├── tests
│ ├── conftest.py
│ ├── test_auth.py
│ ├── test_crud.py
│ ├── test_end_to_end.py
│ └── test_storage.py
└── tox.ini
├── pytest.ini
├── requirements.txt
├── secrets.tar.enc
└── sessions
├── app.yaml
├── main.py
├── main_test.py
├── requirements-dev.txt
└── requirements.txt
/.github/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "config:base",
4 | "group:all",
5 | ":preserveSemverRanges",
6 | ":disableDependencyDashboard"
7 | ],
8 | "ignorePaths": [
9 | "optional-kubernetes-engine"
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/.github/snippet-bot.yml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/.github/sync-repo-settings.yaml:
--------------------------------------------------------------------------------
1 | # Whether or not rebase-merging is enabled on this repository.
2 | # Defaults to `true`
3 | rebaseMergeAllowed: true
4 |
5 | # Whether or not squash-merging is enabled on this repository.
6 | # Defaults to `true`
7 | squashMergeAllowed: true
8 |
9 | # Whether or not PRs are merged with a merge commit on this repository.
10 | # Defaults to `false`
11 | mergeCommitAllowed: false
12 |
13 | # Rules for main branch protection
14 | branchProtectionRules:
15 | # Identifies the protection rule pattern. Name of the branch to be protected.
16 | # Defaults to `main`
17 | - pattern: main
18 | # Can admins overwrite branch protection.
19 | # Defaults to `true`
20 | isAdminEnforced: false
21 | # Number of approving reviews required to update matching branches.
22 | # Defaults to `1`
23 | requiredApprovingReviewCount: 1
24 | # Are reviews from code owners required to update matching branches.
25 | # Defaults to `false`
26 | requiresCodeOwnerReviews: true
27 | # Require up to date branches
28 | requiresStrictStatusChecks: true
29 | # List of required status check contexts that must pass for commits to be accepted to matching branches.
30 | requiredStatusCheckContexts:
31 | - "kokoro"
32 | - "cla/google"
33 | # List of explicit permissions to add (additive only)
34 | permissionRules:
35 | # Team slug to add to repository permissions
36 | - team: yoshi-admins
37 | # Access level required, one of push|pull|admin
38 | permission: admin
39 | - team: python-samples-reviewers
40 | permission: admin
41 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # Distribution / packaging
7 | .Python
8 | env/
9 |
10 | # Installer logs
11 | pip-log.txt
12 | pip-delete-this-directory.txt
13 |
14 | # Unit test / coverage reports
15 | htmlcov/
16 | .tox/
17 | .nox/
18 | .coverage
19 | .coverage.*
20 | .cache
21 | nosetests.xml
22 | coverage.xml
23 | *_log.xml
24 | *,cover
25 | sponge_log.xml
26 |
27 | *.log
28 |
29 | lib/
30 | *.internal
31 | /secrets.tar.gz
32 | /secrets.tar
33 | /client-secret.json
34 | /service-account.json
35 | /config.py
36 |
--------------------------------------------------------------------------------
/.kokoro/common.cfg:
--------------------------------------------------------------------------------
1 | # Format: //devtools/kokoro/config/proto/build.proto
2 |
3 | # Download trampoline resources. These will be in ${KOKORO_GFILE_DIR}
4 | gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline"
5 |
6 | # Download secrets from Cloud Storage.
7 | gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/getting-started-python"
8 |
9 | # All builds use the trampoline script to run in docker.
10 | build_file: "getting-started-python/.kokoro/trampoline_v2.sh"
11 |
12 | # Use the Python worker docker iamge.
13 | env_vars: {
14 | key: "TRAMPOLINE_IMAGE"
15 | value: "gcr.io/cloud-devrel-kokoro-resources/python/getting-started-python"
16 | }
17 |
18 | # Tell the trampoline which build file to use.
19 | env_vars: {
20 | key: "TRAMPOLINE_BUILD_FILE"
21 | value: ".kokoro/system_tests.sh"
22 | }
23 |
24 | # Upload the docker image after successful builds.
25 | env_vars: {
26 | key: "TRAMPOLINE_IMAGE_UPLOAD"
27 | value: "true"
28 | }
29 |
--------------------------------------------------------------------------------
/.kokoro/docker/Dockerfile:
--------------------------------------------------------------------------------
1 | # Copyright 2018 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | FROM gcr.io/cloud-devrel-kokoro-resources/python-base:latest
16 |
17 | # Install libraries needed by third-party python packages that we depend on.
18 | RUN apt-get update \
19 | && apt-get install -y \
20 | graphviz \
21 | libcurl4-openssl-dev \
22 | libffi-dev \
23 | libjpeg-dev \
24 | libmagickwand-dev \
25 | libmemcached-dev \
26 | libmysqlclient-dev \
27 | libpng-dev \
28 | libpq-dev \
29 | libssl-dev \
30 | libxml2-dev \
31 | libxslt1-dev \
32 | openssl \
33 | zlib1g-dev \
34 | && apt-get clean
35 |
36 |
37 | ###################### Check python version
38 |
39 | RUN python3 --version
40 | RUN which python3
41 |
42 | # Setup Cloud SDK
43 | ENV CLOUD_SDK_VERSION 489.0.0
44 | # Use system python for cloud sdk.
45 | ENV CLOUDSDK_PYTHON python3.12
46 | RUN wget https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-sdk-$CLOUD_SDK_VERSION-linux-x86_64.tar.gz
47 | RUN tar xzf google-cloud-sdk-$CLOUD_SDK_VERSION-linux-x86_64.tar.gz
48 | RUN /google-cloud-sdk/install.sh
49 | ENV PATH /google-cloud-sdk/bin:$PATH
50 |
51 | # Setup the user profile for pip
52 | ENV PATH ~/.local/bin:/root/.local/bin:$PATH
53 |
54 | # Install the current version of nox.
55 | RUN python3 -m pip install --user --no-cache-dir nox==2022.1.7
56 |
57 | CMD ["nox"]
58 |
--------------------------------------------------------------------------------
/.kokoro/presubmit.cfg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleCloudPlatform/getting-started-python/d9da8db203f7729f5da28b57be66b69084955bde/.kokoro/presubmit.cfg
--------------------------------------------------------------------------------
/.kokoro/system_tests.cfg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleCloudPlatform/getting-started-python/d9da8db203f7729f5da28b57be66b69084955bde/.kokoro/system_tests.cfg
--------------------------------------------------------------------------------
/.kokoro/system_tests.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Copyright 2017 Google Inc.
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 |
17 | set -eo pipefail
18 |
19 | export PATH=${PATH}:${HOME}/gcloud/google-cloud-sdk/bin
20 |
21 | cd "${PROJECT_ROOT:-github/getting-started-python}"
22 |
23 |
24 | # Unencrypt and extract secrets
25 | SECRETS_PASSWORD=$(cat "${KOKORO_GFILE_DIR}/secrets-password.txt")
26 | ./decrypt-secrets.sh "${SECRETS_PASSWORD}"
27 |
28 | # Setup environment variables
29 | export GOOGLE_APPLICATION_CREDENTIALS="$(pwd)/service-account.json"
30 |
31 | # This block is executed only with Trampoline V2.
32 | if [[ -n "${TRAMPOLINE_VERSION:-}" ]]; then
33 | # Install nox as a user and add it to the PATH.
34 | python3 -m pip install --user nox
35 | export PATH="${PATH}:${HOME}/.local/bin"
36 | fi
37 |
38 | # Run tests
39 | nox -s lint
40 | nox -s run_tests
41 |
42 | # If this is a nightly build, send the test log to the Flaky Bot.
43 | # See https://github.com/googleapis/repo-automation-bots/tree/HEAD/packages/flakybot.
44 | if [[ $KOKORO_BUILD_ARTIFACTS_SUBDIR = *"system_tests"* ]]; then
45 | chmod +x $KOKORO_GFILE_DIR/linux_amd64/flakybot
46 | $KOKORO_GFILE_DIR/linux_amd64/flakybot
47 | fi
48 |
--------------------------------------------------------------------------------
/.kokoro/trampoline.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # Copyright 2017 Google Inc.
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 |
16 | set -eo pipefail
17 |
18 | # Always run the cleanup script, regardless of the success of bouncing into
19 | # the container.
20 |
21 | function cleanup() {
22 | chmod +x ${KOKORO_GFILE_DIR}/trampoline_cleanup.sh
23 | ${KOKORO_GFILE_DIR}/trampoline_cleanup.sh
24 | echo "cleanup";
25 | }
26 | trap cleanup EXIT
27 |
28 | python3 "${KOKORO_GFILE_DIR}/trampoline_v1.py"
29 |
--------------------------------------------------------------------------------
/.kokoro/trampoline_v2.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | # Copyright 2020 Google LLC
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 |
16 | # trampoline_v2.sh
17 | #
18 | # If you want to make a change to this file, consider doing so at:
19 | # https://github.com/googlecloudplatform/docker-ci-helper
20 | #
21 | # This script is for running CI builds. For Kokoro builds, we
22 | # set this script to `build_file` field in the Kokoro configuration.
23 |
24 | # This script does 3 things.
25 | #
26 | # 1. Prepare the Docker image for the test
27 | # 2. Run the Docker with appropriate flags to run the test
28 | # 3. Upload the newly built Docker image
29 | #
30 | # in a way that is somewhat compatible with trampoline_v1.
31 | #
32 | # These environment variables are required:
33 | # TRAMPOLINE_IMAGE: The docker image to use.
34 | # TRAMPOLINE_DOCKERFILE: The location of the Dockerfile.
35 | #
36 | # You can optionally change these environment variables:
37 | # TRAMPOLINE_IMAGE_UPLOAD:
38 | # (true|false): Whether to upload the Docker image after the
39 | # successful builds.
40 | # TRAMPOLINE_BUILD_FILE: The script to run in the docker container.
41 | # TRAMPOLINE_WORKSPACE: The workspace path in the docker container.
42 | # Defaults to /workspace.
43 | # Potentially there are some repo specific envvars in .trampolinerc in
44 | # the project root.
45 | #
46 | # Here is an example for running this script.
47 | # TRAMPOLINE_IMAGE=gcr.io/cloud-devrel-kokoro-resources/node:10-user \
48 | # TRAMPOLINE_BUILD_FILE=.kokoro/system-test.sh \
49 | # .kokoro/trampoline_v2.sh
50 |
51 | set -euo pipefail
52 |
53 | TRAMPOLINE_VERSION="2.0.10"
54 |
55 | if command -v tput >/dev/null && [[ -n "${TERM:-}" ]]; then
56 | readonly IO_COLOR_RED="$(tput setaf 1)"
57 | readonly IO_COLOR_GREEN="$(tput setaf 2)"
58 | readonly IO_COLOR_YELLOW="$(tput setaf 3)"
59 | readonly IO_COLOR_RESET="$(tput sgr0)"
60 | else
61 | readonly IO_COLOR_RED=""
62 | readonly IO_COLOR_GREEN=""
63 | readonly IO_COLOR_YELLOW=""
64 | readonly IO_COLOR_RESET=""
65 | fi
66 |
67 | function function_exists {
68 | [ $(LC_ALL=C type -t $1)"" == "function" ]
69 | }
70 |
71 | # Logs a message using the given color. The first argument must be one
72 | # of the IO_COLOR_* variables defined above, such as
73 | # "${IO_COLOR_YELLOW}". The remaining arguments will be logged in the
74 | # given color. The log message will also have an RFC-3339 timestamp
75 | # prepended (in UTC). You can disable the color output by setting
76 | # TERM=vt100.
77 | function log_impl() {
78 | local color="$1"
79 | shift
80 | local timestamp="$(date -u "+%Y-%m-%dT%H:%M:%SZ")"
81 | echo "================================================================"
82 | echo "${color}${timestamp}:" "$@" "${IO_COLOR_RESET}"
83 | echo "================================================================"
84 | }
85 |
86 | # Logs the given message with normal coloring and a timestamp.
87 | function log() {
88 | log_impl "${IO_COLOR_RESET}" "$@"
89 | }
90 |
91 | # Logs the given message in green with a timestamp.
92 | function log_green() {
93 | log_impl "${IO_COLOR_GREEN}" "$@"
94 | }
95 |
96 | # Logs the given message in yellow with a timestamp.
97 | function log_yellow() {
98 | log_impl "${IO_COLOR_YELLOW}" "$@"
99 | }
100 |
101 | # Logs the given message in red with a timestamp.
102 | function log_red() {
103 | log_impl "${IO_COLOR_RED}" "$@"
104 | }
105 |
106 | readonly tmpdir=$(mktemp -d -t ci-XXXXXXXX)
107 | readonly tmphome="${tmpdir}/h"
108 | mkdir -p "${tmphome}"
109 |
110 | function cleanup() {
111 | rm -rf "${tmpdir}"
112 | }
113 | trap cleanup EXIT
114 |
115 | RUNNING_IN_CI="${RUNNING_IN_CI:-false}"
116 |
117 | # The workspace in the container, defaults to /workspace.
118 | TRAMPOLINE_WORKSPACE="${TRAMPOLINE_WORKSPACE:-/workspace}"
119 |
120 | pass_down_envvars=(
121 | # TRAMPOLINE_V2 variables.
122 | # Tells scripts whether they are running as part of CI or not.
123 | "RUNNING_IN_CI"
124 | # Indicates which CI system we're in.
125 | "TRAMPOLINE_CI"
126 | # Indicates the version of the script.
127 | "TRAMPOLINE_VERSION"
128 | )
129 |
130 | log_yellow "Building with Trampoline ${TRAMPOLINE_VERSION}"
131 |
132 | # Detect which CI systems we're in. If we're in any of the CI systems
133 | # we support, `RUNNING_IN_CI` will be true and `TRAMPOLINE_CI` will be
134 | # the name of the CI system. Both envvars will be passing down to the
135 | # container for telling which CI system we're in.
136 | if [[ -n "${KOKORO_BUILD_ID:-}" ]]; then
137 | # descriptive env var for indicating it's on CI.
138 | RUNNING_IN_CI="true"
139 | TRAMPOLINE_CI="kokoro"
140 | if [[ "${TRAMPOLINE_USE_LEGACY_SERVICE_ACCOUNT:-}" == "true" ]]; then
141 | if [[ ! -f "${KOKORO_GFILE_DIR}/kokoro-trampoline.service-account.json" ]]; then
142 | log_red "${KOKORO_GFILE_DIR}/kokoro-trampoline.service-account.json does not exist. Did you forget to mount cloud-devrel-kokoro-resources/trampoline? Aborting."
143 | exit 1
144 | fi
145 | # This service account will be activated later.
146 | TRAMPOLINE_SERVICE_ACCOUNT="${KOKORO_GFILE_DIR}/kokoro-trampoline.service-account.json"
147 | else
148 | if [[ "${TRAMPOLINE_VERBOSE:-}" == "true" ]]; then
149 | gcloud auth list
150 | fi
151 | log_yellow "Configuring Container Registry access"
152 | gcloud auth configure-docker --quiet
153 | fi
154 | pass_down_envvars+=(
155 | # KOKORO dynamic variables.
156 | "KOKORO_BUILD_NUMBER"
157 | "KOKORO_BUILD_ID"
158 | "KOKORO_JOB_NAME"
159 | "KOKORO_GIT_COMMIT"
160 | "KOKORO_GITHUB_COMMIT"
161 | "KOKORO_GITHUB_PULL_REQUEST_NUMBER"
162 | "KOKORO_GITHUB_PULL_REQUEST_COMMIT"
163 | # For Flaky Bot
164 | "KOKORO_GITHUB_COMMIT_URL"
165 | "KOKORO_GITHUB_PULL_REQUEST_URL"
166 | "KOKORO_BUILD_ARTIFACTS_SUBDIR"
167 | )
168 | elif [[ "${TRAVIS:-}" == "true" ]]; then
169 | RUNNING_IN_CI="true"
170 | TRAMPOLINE_CI="travis"
171 | pass_down_envvars+=(
172 | "TRAVIS_BRANCH"
173 | "TRAVIS_BUILD_ID"
174 | "TRAVIS_BUILD_NUMBER"
175 | "TRAVIS_BUILD_WEB_URL"
176 | "TRAVIS_COMMIT"
177 | "TRAVIS_COMMIT_MESSAGE"
178 | "TRAVIS_COMMIT_RANGE"
179 | "TRAVIS_JOB_NAME"
180 | "TRAVIS_JOB_NUMBER"
181 | "TRAVIS_JOB_WEB_URL"
182 | "TRAVIS_PULL_REQUEST"
183 | "TRAVIS_PULL_REQUEST_BRANCH"
184 | "TRAVIS_PULL_REQUEST_SHA"
185 | "TRAVIS_PULL_REQUEST_SLUG"
186 | "TRAVIS_REPO_SLUG"
187 | "TRAVIS_SECURE_ENV_VARS"
188 | "TRAVIS_TAG"
189 | )
190 | elif [[ -n "${GITHUB_RUN_ID:-}" ]]; then
191 | RUNNING_IN_CI="true"
192 | TRAMPOLINE_CI="github-workflow"
193 | pass_down_envvars+=(
194 | "GITHUB_WORKFLOW"
195 | "GITHUB_RUN_ID"
196 | "GITHUB_RUN_NUMBER"
197 | "GITHUB_ACTION"
198 | "GITHUB_ACTIONS"
199 | "GITHUB_ACTOR"
200 | "GITHUB_REPOSITORY"
201 | "GITHUB_EVENT_NAME"
202 | "GITHUB_EVENT_PATH"
203 | "GITHUB_SHA"
204 | "GITHUB_REF"
205 | "GITHUB_HEAD_REF"
206 | "GITHUB_BASE_REF"
207 | )
208 | elif [[ "${CIRCLECI:-}" == "true" ]]; then
209 | RUNNING_IN_CI="true"
210 | TRAMPOLINE_CI="circleci"
211 | pass_down_envvars+=(
212 | "CIRCLE_BRANCH"
213 | "CIRCLE_BUILD_NUM"
214 | "CIRCLE_BUILD_URL"
215 | "CIRCLE_COMPARE_URL"
216 | "CIRCLE_JOB"
217 | "CIRCLE_NODE_INDEX"
218 | "CIRCLE_NODE_TOTAL"
219 | "CIRCLE_PREVIOUS_BUILD_NUM"
220 | "CIRCLE_PROJECT_REPONAME"
221 | "CIRCLE_PROJECT_USERNAME"
222 | "CIRCLE_REPOSITORY_URL"
223 | "CIRCLE_SHA1"
224 | "CIRCLE_STAGE"
225 | "CIRCLE_USERNAME"
226 | "CIRCLE_WORKFLOW_ID"
227 | "CIRCLE_WORKFLOW_JOB_ID"
228 | "CIRCLE_WORKFLOW_UPSTREAM_JOB_IDS"
229 | "CIRCLE_WORKFLOW_WORKSPACE_ID"
230 | )
231 | fi
232 |
233 | # Configure the service account for pulling the docker image.
234 | function repo_root() {
235 | local dir="$1"
236 | while [[ ! -d "${dir}/.git" ]]; do
237 | dir="$(dirname "$dir")"
238 | done
239 | echo "${dir}"
240 | }
241 |
242 | # Detect the project root. In CI builds, we assume the script is in
243 | # the git tree and traverse from there, otherwise, traverse from `pwd`
244 | # to find `.git` directory.
245 | if [[ "${RUNNING_IN_CI:-}" == "true" ]]; then
246 | PROGRAM_PATH="$(realpath "$0")"
247 | PROGRAM_DIR="$(dirname "${PROGRAM_PATH}")"
248 | PROJECT_ROOT="$(repo_root "${PROGRAM_DIR}")"
249 | else
250 | PROJECT_ROOT="$(repo_root $(pwd))"
251 | fi
252 |
253 | log_yellow "Changing to the project root: ${PROJECT_ROOT}."
254 | cd "${PROJECT_ROOT}"
255 |
256 | # To support relative path for `TRAMPOLINE_SERVICE_ACCOUNT`, we need
257 | # to use this environment variable in `PROJECT_ROOT`.
258 | if [[ -n "${TRAMPOLINE_SERVICE_ACCOUNT:-}" ]]; then
259 |
260 | mkdir -p "${tmpdir}/gcloud"
261 | gcloud_config_dir="${tmpdir}/gcloud"
262 |
263 | log_yellow "Using isolated gcloud config: ${gcloud_config_dir}."
264 | export CLOUDSDK_CONFIG="${gcloud_config_dir}"
265 |
266 | log_yellow "Using ${TRAMPOLINE_SERVICE_ACCOUNT} for authentication."
267 | gcloud auth activate-service-account \
268 | --key-file "${TRAMPOLINE_SERVICE_ACCOUNT}"
269 | log_yellow "Configuring Container Registry access"
270 | gcloud auth configure-docker --quiet
271 | fi
272 |
273 | required_envvars=(
274 | # The basic trampoline configurations.
275 | "TRAMPOLINE_IMAGE"
276 | "TRAMPOLINE_BUILD_FILE"
277 | )
278 |
279 | if [[ -f "${PROJECT_ROOT}/.trampolinerc" ]]; then
280 | source "${PROJECT_ROOT}/.trampolinerc"
281 | fi
282 |
283 | log_yellow "Checking environment variables."
284 | for e in "${required_envvars[@]}"
285 | do
286 | if [[ -z "${!e:-}" ]]; then
287 | log "Missing ${e} env var. Aborting."
288 | exit 1
289 | fi
290 | done
291 |
292 | # We want to support legacy style TRAMPOLINE_BUILD_FILE used with V1
293 | # script: e.g. "github/repo-name/.kokoro/run_tests.sh"
294 | TRAMPOLINE_BUILD_FILE="${TRAMPOLINE_BUILD_FILE#github/*/}"
295 | log_yellow "Using TRAMPOLINE_BUILD_FILE: ${TRAMPOLINE_BUILD_FILE}"
296 |
297 | # ignore error on docker operations and test execution
298 | set +e
299 |
300 | log_yellow "Preparing Docker image."
301 | # We only download the docker image in CI builds.
302 | if [[ "${RUNNING_IN_CI:-}" == "true" ]]; then
303 | # Download the docker image specified by `TRAMPOLINE_IMAGE`
304 |
305 | # We may want to add --max-concurrent-downloads flag.
306 |
307 | log_yellow "Start pulling the Docker image: ${TRAMPOLINE_IMAGE}."
308 | if docker pull "${TRAMPOLINE_IMAGE}"; then
309 | log_green "Finished pulling the Docker image: ${TRAMPOLINE_IMAGE}."
310 | has_image="true"
311 | else
312 | log_red "Failed pulling the Docker image: ${TRAMPOLINE_IMAGE}."
313 | has_image="false"
314 | fi
315 | else
316 | # For local run, check if we have the image.
317 | if docker images "${TRAMPOLINE_IMAGE}" | grep "${TRAMPOLINE_IMAGE%:*}"; then
318 | has_image="true"
319 | else
320 | has_image="false"
321 | fi
322 | fi
323 |
324 |
325 | # The default user for a Docker container has uid 0 (root). To avoid
326 | # creating root-owned files in the build directory we tell docker to
327 | # use the current user ID.
328 | user_uid="$(id -u)"
329 | user_gid="$(id -g)"
330 | user_name="$(id -un)"
331 |
332 | # To allow docker in docker, we add the user to the docker group in
333 | # the host os.
334 | docker_gid=$(cut -d: -f3 < <(getent group docker))
335 |
336 | update_cache="false"
337 | if [[ "${TRAMPOLINE_DOCKERFILE:-none}" != "none" ]]; then
338 | # Build the Docker image from the source.
339 | context_dir=$(dirname "${TRAMPOLINE_DOCKERFILE}")
340 | docker_build_flags=(
341 | "-f" "${TRAMPOLINE_DOCKERFILE}"
342 | "-t" "${TRAMPOLINE_IMAGE}"
343 | "--build-arg" "UID=${user_uid}"
344 | "--build-arg" "USERNAME=${user_name}"
345 | )
346 | if [[ "${has_image}" == "true" ]]; then
347 | docker_build_flags+=("--cache-from" "${TRAMPOLINE_IMAGE}")
348 | fi
349 |
350 | log_yellow "Start building the docker image."
351 | if [[ "${TRAMPOLINE_VERBOSE:-false}" == "true" ]]; then
352 | echo "docker build" "${docker_build_flags[@]}" "${context_dir}"
353 | fi
354 |
355 | # ON CI systems, we want to suppress docker build logs, only
356 | # output the logs when it fails.
357 | if [[ "${RUNNING_IN_CI:-}" == "true" ]]; then
358 | if docker build "${docker_build_flags[@]}" "${context_dir}" \
359 | > "${tmpdir}/docker_build.log" 2>&1; then
360 | if [[ "${TRAMPOLINE_VERBOSE:-}" == "true" ]]; then
361 | cat "${tmpdir}/docker_build.log"
362 | fi
363 |
364 | log_green "Finished building the docker image."
365 | update_cache="true"
366 | else
367 | log_red "Failed to build the Docker image, aborting."
368 | log_yellow "Dumping the build logs:"
369 | cat "${tmpdir}/docker_build.log"
370 | exit 1
371 | fi
372 | else
373 | if docker build "${docker_build_flags[@]}" "${context_dir}"; then
374 | log_green "Finished building the docker image."
375 | update_cache="true"
376 | else
377 | log_red "Failed to build the Docker image, aborting."
378 | exit 1
379 | fi
380 | fi
381 | else
382 | if [[ "${has_image}" != "true" ]]; then
383 | log_red "We do not have ${TRAMPOLINE_IMAGE} locally, aborting."
384 | exit 1
385 | fi
386 | fi
387 |
388 | # We use an array for the flags so they are easier to document.
389 | docker_flags=(
390 | # Remove the container after it exists.
391 | "--rm"
392 |
393 | # Use the host network.
394 | "--network=host"
395 |
396 | # Run in priviledged mode. We are not using docker for sandboxing or
397 | # isolation, just for packaging our dev tools.
398 | "--privileged"
399 |
400 | # Run the docker script with the user id. Because the docker image gets to
401 | # write in ${PWD} you typically want this to be your user id.
402 | # To allow docker in docker, we need to use docker gid on the host.
403 | "--user" "${user_uid}:${docker_gid}"
404 |
405 | # Pass down the USER.
406 | "--env" "USER=${user_name}"
407 |
408 | # Mount the project directory inside the Docker container.
409 | "--volume" "${PROJECT_ROOT}:${TRAMPOLINE_WORKSPACE}"
410 | "--workdir" "${TRAMPOLINE_WORKSPACE}"
411 | "--env" "PROJECT_ROOT=${TRAMPOLINE_WORKSPACE}"
412 |
413 | # Mount the temporary home directory.
414 | "--volume" "${tmphome}:/h"
415 | "--env" "HOME=/h"
416 |
417 | # Allow docker in docker.
418 | "--volume" "/var/run/docker.sock:/var/run/docker.sock"
419 |
420 | # Mount the /tmp so that docker in docker can mount the files
421 | # there correctly.
422 | "--volume" "/tmp:/tmp"
423 | # Pass down the KOKORO_GFILE_DIR and KOKORO_KEYSTORE_DIR
424 | # TODO(tmatsuo): This part is not portable.
425 | "--env" "TRAMPOLINE_SECRET_DIR=/secrets"
426 | "--volume" "${KOKORO_GFILE_DIR:-/dev/shm}:/secrets/gfile"
427 | "--env" "KOKORO_GFILE_DIR=/secrets/gfile"
428 | "--volume" "${KOKORO_KEYSTORE_DIR:-/dev/shm}:/secrets/keystore"
429 | "--env" "KOKORO_KEYSTORE_DIR=/secrets/keystore"
430 | )
431 |
432 | # Add an option for nicer output if the build gets a tty.
433 | if [[ -t 0 ]]; then
434 | docker_flags+=("-it")
435 | fi
436 |
437 | # Passing down env vars
438 | for e in "${pass_down_envvars[@]}"
439 | do
440 | if [[ -n "${!e:-}" ]]; then
441 | docker_flags+=("--env" "${e}=${!e}")
442 | fi
443 | done
444 |
445 | # If arguments are given, all arguments will become the commands run
446 | # in the container, otherwise run TRAMPOLINE_BUILD_FILE.
447 | if [[ $# -ge 1 ]]; then
448 | log_yellow "Running the given commands '" "${@:1}" "' in the container."
449 | readonly commands=("${@:1}")
450 | if [[ "${TRAMPOLINE_VERBOSE:-}" == "true" ]]; then
451 | echo docker run "${docker_flags[@]}" "${TRAMPOLINE_IMAGE}" "${commands[@]}"
452 | fi
453 | docker run "${docker_flags[@]}" "${TRAMPOLINE_IMAGE}" "${commands[@]}"
454 | else
455 | log_yellow "Running the tests in a Docker container."
456 | docker_flags+=("--entrypoint=${TRAMPOLINE_BUILD_FILE}")
457 | if [[ "${TRAMPOLINE_VERBOSE:-}" == "true" ]]; then
458 | echo docker run "${docker_flags[@]}" "${TRAMPOLINE_IMAGE}"
459 | fi
460 | docker run "${docker_flags[@]}" "${TRAMPOLINE_IMAGE}"
461 | fi
462 |
463 |
464 | test_retval=$?
465 |
466 | if [[ ${test_retval} -eq 0 ]]; then
467 | log_green "Build finished with ${test_retval}"
468 | else
469 | log_red "Build finished with ${test_retval}"
470 | fi
471 |
472 | # Only upload it when the test passes.
473 | if [[ "${update_cache}" == "true" ]] && \
474 | [[ $test_retval == 0 ]] && \
475 | [[ "${TRAMPOLINE_IMAGE_UPLOAD:-false}" == "true" ]]; then
476 | log_yellow "Uploading the Docker image."
477 | if docker push "${TRAMPOLINE_IMAGE}"; then
478 | log_green "Finished uploading the Docker image."
479 | else
480 | log_red "Failed uploading the Docker image."
481 | fi
482 | # Call trampoline_after_upload_hook if it's defined.
483 | if function_exists trampoline_after_upload_hook; then
484 | trampoline_after_upload_hook
485 | fi
486 |
487 | fi
488 |
489 | exit "${test_retval}"
490 |
--------------------------------------------------------------------------------
/.trampolinerc:
--------------------------------------------------------------------------------
1 | # Copyright 2020 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | # Add required env vars here.
16 | required_envvars+=(
17 | )
18 |
19 | # Add env vars which are passed down into the container here.
20 | pass_down_envvars+=(
21 | # We test this envvar in tests/python/test_envvar.py.
22 | "TEST_ENV"
23 | )
24 |
25 | # Prevent unintentional override on the default image.
26 | if [[ "${TRAMPOLINE_IMAGE_UPLOAD:-false}" == "true" ]] && \
27 | [[ -z "${TRAMPOLINE_IMAGE:-}" ]]; then
28 | echo "Please set TRAMPOLINE_IMAGE if you want to upload the Docker image."
29 | exit 1
30 | fi
31 |
32 | # Define the default value if it makes sense.
33 | if [[ -z "${TRAMPOLINE_IMAGE_UPLOAD:-}" ]]; then
34 | TRAMPOLINE_IMAGE_UPLOAD=""
35 | fi
36 |
37 | if [[ -z "${TRAMPOLINE_IMAGE:-}" ]]; then
38 | TRAMPOLINE_IMAGE=""
39 | fi
40 |
41 | if [[ -z "${TRAMPOLINE_DOCKERFILE:-}" ]]; then
42 | TRAMPOLINE_DOCKERFILE=".kokoro/docker/Dockerfile"
43 | fi
44 |
45 | if [[ -z "${TRAMPOLINE_BUILD_FILE:-}" ]]; then
46 | TRAMPOLINE_BUILD_FILE=""
47 | fi
48 |
49 | # The build will show some commands and docker build logs.
50 | TRAMPOLINE_VERBOSE="true"
51 |
--------------------------------------------------------------------------------
/CODEOWNERS:
--------------------------------------------------------------------------------
1 | # Code owners file.
2 | # This file controls who is tagged for review for any given pull request.
3 | #
4 | # For syntax help see:
5 | # https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners#codeowners-syntax
6 |
7 |
8 | # The python-samples-owners team is the default owner for anything not
9 | # explicitly taken by someone else.
10 | * @GoogleCloudPlatform/python-samples-reviewers
11 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # How to become a contributor and submit your own code
2 |
3 | ## Contributor License Agreements
4 |
5 | We'd love to accept your sample apps and patches! Before we can take them, we
6 | have to jump a couple of legal hurdles.
7 |
8 | Please fill out either the individual or corporate Contributor License Agreement
9 | (CLA):
10 |
11 | * If you are an individual writing original source code and you're sure you
12 | own the intellectual property, then you'll need to sign an [individual CLA](https://developers.google.com/open-source/cla/individual).
13 | * If you work for a company that wants to allow you to contribute your work,
14 | then you'll need to sign a [corporate CLA](https://developers.google.com/open-source/cla/corporate).
15 |
16 | Follow either of the two links above to access the appropriate CLA and
17 | instructions for how to sign and return it. Once we receive it, we'll be able to
18 | accept your pull requests.
19 |
20 | ## Contributing A Patch
21 |
22 | 1. Submit an issue describing your proposed change to the repository in question.
23 | 1. The repository owner will respond to your issue promptly.
24 | 1. If your proposed change is accepted, and you haven't already done so, sign a
25 | CLA (see details above).
26 | 1. Fork the desired repo, then develop and test your code changes.
27 | 1. Ensure that your code adheres to the existing style in the sample to which
28 | you are contributing. Refer to the [Google Python Style Guide](https://github.com/google/styleguide/blob/gh-pages/pyguide.md) and the
29 | [Google Cloud Platform Community Style Guide](https://cloud.google.com/community/tutorials/styleguide) for the
30 | recommended coding standards for this organization.
31 | 1. Ensure that your code has an appropriate set of unit tests which all pass.
32 | 1. Submit a pull request.
33 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Getting started with Python on Google Cloud Platform
2 |
3 | This repository is the complete sample code for the [Python Getting Started on Google Cloud Platform](https://cloud.google.com/python/docs/) tutorials. Please refer to the tutorials for instructions on configuring, running, and deploying these samples.
4 |
5 | The code for the samples is contained in individual folders in this repository.
6 |
7 | Tutorial | Folder
8 | ---------|-------
9 | [Getting Started](https://cloud.google.com/python/getting-started/) | [bookshelf](https://github.com/GoogleCloudPlatform/getting-started-python/tree/main/bookshelf)
10 | [Background Processing](https://cloud.google.com/python/getting-started/background-processing) | [background](https://github.com/GoogleCloudPlatform/getting-started-python/tree/main/background)
11 | [Deploying to Google Kubernetes Engine](https://cloud.google.com/kubernetes-engine/docs/quickstarts/deploying-a-language-specific-app) | [in "kubernetes-engine-samples" repo](https://github.com/GoogleCloudPlatform/kubernetes-engine-samples/tree/main/quickstart/python)
12 | [Deploying to Google Compute Engine](https://cloud.google.com/python/tutorials/getting-started-on-compute-engine) | [gce](https://github.com/GoogleCloudPlatform/getting-started-python/tree/main/gce)
13 | [Handling Sessions with Firestore](https://cloud.google.com/python/getting-started/session-handling-with-firestore) | [sessions](https://github.com/GoogleCloudPlatform/getting-started-python/tree/main/sessions)
14 | [Authenticating Users with IAP](https://cloud.google.com/python/getting-started/authenticate-users) | [authenticating-users](https://github.com/GoogleCloudPlatform/getting-started-python/tree/main/authenticating-users)
15 |
16 | ## Contributing changes
17 |
18 | * See [CONTRIBUTING.md](CONTRIBUTING.md)
19 |
20 | ## Licensing
21 |
22 | * See [LICENSE](LICENSE)
23 |
--------------------------------------------------------------------------------
/authenticating-users/app.yaml:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Google LLC All Rights Reserved.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | # [START getting_started_app_yaml]
16 | runtime: python37
17 | # [END getting_started_app_yaml]
18 |
--------------------------------------------------------------------------------
/authenticating-users/main.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Google LLC All Rights Reserved.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | # [START getting_started_auth_all]
16 | import sys
17 |
18 | from flask import Flask
19 | app = Flask(__name__)
20 |
21 | CERTS = None
22 | AUDIENCE = None
23 |
24 |
25 | # [START getting_started_auth_certs]
26 | def certs():
27 | """Returns a dictionary of current Google public key certificates for
28 | validating Google-signed JWTs. Since these change rarely, the result
29 | is cached on first request for faster subsequent responses.
30 | """
31 | import requests
32 |
33 | global CERTS
34 | if CERTS is None:
35 | response = requests.get(
36 | 'https://www.gstatic.com/iap/verify/public_key'
37 | )
38 | CERTS = response.json()
39 | return CERTS
40 | # [END getting_started_auth_certs]
41 |
42 |
43 | # [START getting_started_auth_metadata]
44 | def get_metadata(item_name):
45 | """Returns a string with the project metadata value for the item_name.
46 | See https://cloud.google.com/compute/docs/storing-retrieving-metadata for
47 | possible item_name values.
48 | """
49 | import requests
50 |
51 | endpoint = 'http://metadata.google.internal'
52 | path = '/computeMetadata/v1/project/'
53 | path += item_name
54 | response = requests.get(
55 | '{}{}'.format(endpoint, path),
56 | headers={'Metadata-Flavor': 'Google'}
57 | )
58 | metadata = response.text
59 | return metadata
60 | # [END getting_started_auth_metadata]
61 |
62 |
63 | # [START getting_started_auth_audience]
64 | def audience():
65 | """Returns the audience value (the JWT 'aud' property) for the current
66 | running instance. Since this involves a metadata lookup, the result is
67 | cached when first requested for faster future responses.
68 | """
69 | global AUDIENCE
70 | if AUDIENCE is None:
71 | project_number = get_metadata('numeric-project-id')
72 | project_id = get_metadata('project-id')
73 | AUDIENCE = '/projects/{}/apps/{}'.format(
74 | project_number, project_id
75 | )
76 | return AUDIENCE
77 | # [END getting_started_auth_audience]
78 |
79 |
80 | # [START getting_started_auth_validate_assertion]
81 | def validate_assertion(assertion):
82 | """Checks that the JWT assertion is valid (properly signed, for the
83 | correct audience) and if so, returns strings for the requesting user's
84 | email and a persistent user ID. If not valid, returns None for each field.
85 | """
86 | from jose import jwt
87 |
88 | try:
89 | info = jwt.decode(
90 | assertion,
91 | certs(),
92 | algorithms=['ES256'],
93 | audience=audience()
94 | )
95 | return info['email'], info['sub']
96 | except Exception as e:
97 | print('Failed to validate assertion: {}'.format(e), file=sys.stderr)
98 | return None, None
99 | # [END getting_started_auth_validate_assertion]
100 |
101 |
102 | # [START getting_started_auth_front_controller]
103 | @app.route('/', methods=['GET'])
104 | def say_hello():
105 | from flask import request
106 |
107 | assertion = request.headers.get('X-Goog-IAP-JWT-Assertion')
108 | email, id = validate_assertion(assertion)
109 | page = "
Hello {}
".format(email)
110 | return page
111 | # [END getting_started_auth_front_controller]
112 | # [END getting_started_auth_all]
113 |
--------------------------------------------------------------------------------
/authenticating-users/main_test.py:
--------------------------------------------------------------------------------
1 | # Copyright 2022 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import main
16 |
17 |
18 | def fake_validate(assertion):
19 | if assertion == "Valid":
20 | return "nobody@example.com", "user0001"
21 | else:
22 | return None, None
23 |
24 |
25 | main.validate_assertion = fake_validate
26 |
27 |
28 | def test_home_page():
29 | client = main.app.test_client()
30 |
31 | # Good request check
32 | r = client.get("/", headers={"X-Goog-IAP-JWT-Assertion": "Valid"})
33 | assert "nobody@example.com" in r.text
34 |
35 | # Missing header check
36 | r = client.get("/")
37 | assert "None" in r.text
38 |
39 | # Bad header check
40 | r = client.get("/", headers={"X-Goog-IAP-JWT-Assertion": "Not Valid"})
41 | assert "None" in r.text
42 |
--------------------------------------------------------------------------------
/authenticating-users/requirements-test.txt:
--------------------------------------------------------------------------------
1 | pytest==7.1.2
--------------------------------------------------------------------------------
/authenticating-users/requirements.txt:
--------------------------------------------------------------------------------
1 | # [START getting_started_requirements]
2 | Flask==2.2.5
3 | cryptography==41.0.2
4 | python-jose[cryptography]==3.3.0
5 | requests==2.31.0
6 | # [END getting_started_requirements]
7 |
--------------------------------------------------------------------------------
/background/README.md:
--------------------------------------------------------------------------------
1 | Background Processing
2 | ---------------------
3 |
4 | This directory contains an example of doing background processing with App
5 | Engine, Cloud Pub/Sub, Cloud Functions, and Firestore.
6 |
7 | Deploy commands:
8 |
9 | From the app directory:
10 | ```
11 | $ gcloud app deploy
12 | ```
13 |
14 | From the function directory, after creating the PubSub topic:
15 | ```
16 | $ gcloud functions deploy --runtime=python37 --trigger-topic=translate Translate --set-env-vars GOOGLE_CLOUD_PROJECT=my-project
17 | ```
18 |
--------------------------------------------------------------------------------
/background/app/app.yaml:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Google LLC All Rights Reserved.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | # [START getting_started_background_config]
16 | runtime: python37
17 | # [END getting_started_background_config]
18 |
--------------------------------------------------------------------------------
/background/app/main.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Google LLC All Rights Reserved.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """ This web app shows translations that have been previously requested, and
16 | provides a form to request a new translation.
17 | """
18 |
19 | # [START getting_started_background_app_main]
20 | import json
21 | import os
22 |
23 | from flask import Flask, redirect, render_template, request
24 | from google.cloud import firestore
25 | from google.cloud import pubsub
26 |
27 |
28 | app = Flask(__name__)
29 |
30 | # Get client objects to reuse over multiple invocations
31 | db = firestore.Client()
32 | publisher = pubsub.PublisherClient()
33 |
34 | # Keep this list of supported languages up to date
35 | ACCEPTABLE_LANGUAGES = ("de", "en", "es", "fr", "ja", "sw")
36 | # [END getting_started_background_app_main]
37 |
38 |
39 | # [START getting_started_background_app_list]
40 | @app.route("/", methods=["GET"])
41 | def index():
42 | """The home page has a list of prior translations and a form to
43 | ask for a new translation.
44 | """
45 |
46 | doc_list = []
47 | docs = db.collection("translations").stream()
48 | for doc in docs:
49 | doc_list.append(doc.to_dict())
50 |
51 | return render_template("index.html", translations=doc_list)
52 |
53 |
54 | # [END getting_started_background_app_list]
55 |
56 |
57 | # [START getting_started_background_app_request]
58 | @app.route("/request-translation", methods=["POST"])
59 | def translate():
60 | """Handle a request to translate a string (form field 'v') to a given
61 | language (form field 'lang'), by sending a PubSub message to a topic.
62 | """
63 | source_string = request.form.get("v", "")
64 | to_language = request.form.get("lang", "")
65 |
66 | if source_string == "":
67 | error_message = "Empty value"
68 | return error_message, 400
69 |
70 | if to_language not in ACCEPTABLE_LANGUAGES:
71 | error_message = "Unsupported language: {}".format(to_language)
72 | return error_message, 400
73 |
74 | message = {
75 | "Original": source_string,
76 | "Language": to_language,
77 | "Translated": "",
78 | "OriginalLanguage": "",
79 | }
80 |
81 | topic_name = "projects/{}/topics/{}".format(
82 | os.getenv("GOOGLE_CLOUD_PROJECT"), "translate"
83 | )
84 | publisher.publish(
85 | topic=topic_name, data=json.dumps(message).encode("utf8")
86 | )
87 | return redirect("/")
88 |
89 |
90 | # [END getting_started_background_app_request]
91 |
--------------------------------------------------------------------------------
/background/app/main_test.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Google LLC All Rights Reserved.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import os
16 | import uuid
17 |
18 | import google.auth
19 | from google.cloud import firestore
20 | from google.cloud import pubsub
21 | import main
22 | import pytest
23 |
24 |
25 | credentials, project_id = google.auth.default()
26 | os.environ["GOOGLE_CLOUD_PROJECT"] = project_id
27 | SUBSCRIPTION_NAME = "projects/{}/subscriptions/{}".format(
28 | project_id, "test-" + str(uuid.uuid4())
29 | )
30 | TOPIC_NAME = "projects/{}/topics/{}".format(project_id, "translate")
31 |
32 |
33 | @pytest.fixture
34 | def db():
35 | def clear_collection(collection):
36 | """Removes every document from the collection, to make it easy to see
37 | what has been added by the current test run.
38 | """
39 | for doc in collection.stream():
40 | doc.reference.delete()
41 |
42 | client = firestore.Client()
43 | translations = client.collection("translations")
44 | clear_collection(translations)
45 | translations.add(
46 | {
47 | "Original": "A testing message",
48 | "Language": "fr",
49 | "Translated": '"A testing message", but in French',
50 | "OriginalLanguage": "en",
51 | },
52 | document_id="test translation",
53 | )
54 | yield client
55 |
56 |
57 | @pytest.fixture
58 | def publisher():
59 | client = pubsub.PublisherClient()
60 | yield client
61 |
62 |
63 | @pytest.fixture
64 | def subscriber():
65 | subscriber = pubsub.SubscriberClient()
66 | subscriber.create_subscription(
67 | request={"name": SUBSCRIPTION_NAME, "topic": TOPIC_NAME}
68 | )
69 | yield subscriber
70 | subscriber.delete_subscription(request={"subscription": SUBSCRIPTION_NAME})
71 |
72 |
73 | def test_index(db, publisher):
74 | main.app.testing = True
75 | main.db = db
76 | main.publisher = publisher
77 | client = main.app.test_client()
78 |
79 | r = client.get("/")
80 | assert r.status_code == 200
81 | response_text = r.data.decode("utf-8")
82 | assert "Text to translate" in response_text
83 | assert "but in French" in response_text
84 |
85 |
86 | def test_translate(db, publisher, subscriber):
87 | main.app.testing = True
88 | main.db = db
89 | main.publisher = publisher
90 | client = main.app.test_client()
91 |
92 | r = client.post(
93 | "/request-translation",
94 | data={
95 | "v": "This is a test",
96 | "lang": "fr",
97 | },
98 | )
99 |
100 | assert r.status_code < 400
101 |
102 | response = subscriber.pull(
103 | request={"subscription": SUBSCRIPTION_NAME, "max_messages": 1},
104 | timeout=10.0,
105 | )
106 | assert len(response.received_messages) == 1
107 | assert b"This is a test" in response.received_messages[0].message.data
108 | assert b"fr" in response.received_messages[0].message.data
109 |
--------------------------------------------------------------------------------
/background/app/requirements.txt:
--------------------------------------------------------------------------------
1 | google-cloud-firestore==2.11.1
2 | google-cloud-pubsub==2.16.1
3 | flask==2.2.5
4 |
--------------------------------------------------------------------------------
/background/app/templates/index.html:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | Translations
20 |
21 |
22 |
23 |
24 |
25 |
61 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 | Translate with Background Processing
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
101 |
102 |
103 |
104 |
105 |
106 |
Original
107 |
Translation
108 |
109 |
110 |
111 | {% for translation in translations %}
112 |
142 |
143 |
144 |
145 |
146 |
--------------------------------------------------------------------------------
/background/function/main.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Google LLC All Rights Reserved.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """ This function handles messages posted to a pubsub topic by translating
16 | the data in the message as requested. The message must be a JSON encoded
17 | dictionary with fields:
18 |
19 | Original - the string to translate
20 | Language - the language to translate the string to
21 |
22 | The dictionary may have other fields, which will be ignored.
23 | """
24 |
25 | # [START getting_started_background_translate_setup]
26 | import base64
27 | import hashlib
28 | import json
29 |
30 | from google.cloud import firestore
31 | from google.cloud import translate_v2 as translate
32 | # [END getting_started_background_translate_setup]
33 |
34 | # [START getting_started_background_translate_init]
35 | # Get client objects once to reuse over multiple invocations.
36 | xlate = translate.Client()
37 | db = firestore.Client()
38 | # [END getting_started_background_translate_init]
39 |
40 |
41 | # [START getting_started_background_translate_string]
42 | def translate_string(from_string, to_language):
43 | """ Translates a string to a specified language.
44 |
45 | from_string - the original string before translation
46 |
47 | to_language - the language to translate to, as a two-letter code (e.g.,
48 | 'en' for english, 'de' for german)
49 |
50 | Returns the translated string and the code for original language
51 | """
52 | result = xlate.translate(from_string, target_language=to_language)
53 | return result['translatedText'], result['detectedSourceLanguage']
54 | # [END getting_started_background_translate_string]
55 |
56 |
57 | # [START getting_started_background_translate]
58 | def document_name(message):
59 | """ Messages are saved in a Firestore database with document IDs generated
60 | from the original string and destination language. If the exact same
61 | translation is requested a second time, the result will overwrite the
62 | prior result.
63 |
64 | message - a dictionary with fields named Language and Original, and
65 | optionally other fields with any names
66 |
67 | Returns a unique name that is an allowed Firestore document ID
68 | """
69 | key = '{}/{}'.format(message['Language'], message['Original'])
70 | hashed = hashlib.sha512(key.encode()).digest()
71 |
72 | # Note that document IDs should not contain the '/' character
73 | name = base64.b64encode(hashed, altchars=b'+-').decode('utf-8')
74 | return name
75 |
76 |
77 | @firestore.transactional
78 | def update_database(transaction, message):
79 | name = document_name(message)
80 | doc_ref = db.collection('translations').document(document_id=name)
81 |
82 | try:
83 | doc_ref.get(transaction=transaction)
84 | except firestore.NotFound:
85 | return # Don't replace an existing translation
86 |
87 | transaction.set(doc_ref, message)
88 |
89 |
90 | def translate_message(event, context):
91 | """ Process a pubsub message requesting a translation
92 | """
93 | message_data = base64.b64decode(event['data']).decode('utf-8')
94 | message = json.loads(message_data)
95 |
96 | from_string = message['Original']
97 | to_language = message['Language']
98 |
99 | to_string, from_language = translate_string(from_string, to_language)
100 |
101 | message['Translated'] = to_string
102 | message['OriginalLanguage'] = from_language
103 |
104 | transaction = db.transaction()
105 | update_database(transaction, message)
106 | # [END getting_started_background_translate]
107 |
--------------------------------------------------------------------------------
/background/function/main_test.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Google LLC All Rights Reserved.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import base64
16 | import json
17 |
18 | from google.cloud import firestore
19 | import main
20 |
21 |
22 | def clear_collection(collection):
23 | """ Removes every document from the collection, to make it easy to see
24 | what has been added by the current test run.
25 | """
26 | for doc in collection.stream():
27 | doc.reference.delete()
28 |
29 |
30 | def test_invocations():
31 | db = firestore.Client()
32 | main.db = db
33 |
34 | translations = db.collection('translations')
35 | clear_collection(translations)
36 |
37 | event = {
38 | 'data': base64.b64encode(json.dumps({
39 | 'Original': 'My test message',
40 | 'Language': 'de',
41 | }).encode('utf-8'))
42 | }
43 |
44 | main.translate_message(event, None)
45 |
46 | docs = [doc for doc in translations.stream()]
47 | assert len(docs) == 1 # Should be only the one just created
48 |
49 | message = docs[0].to_dict()
50 |
51 | assert message['Original'] == 'My test message'
52 | assert message['Language'] == 'de'
53 | assert len(message['Translated']) > 0
54 | assert message['OriginalLanguage'] == 'en'
55 |
--------------------------------------------------------------------------------
/background/function/requirements.txt:
--------------------------------------------------------------------------------
1 | google-cloud-translate==3.11.1
2 | google-cloud-firestore==2.11.1
3 |
--------------------------------------------------------------------------------
/bookshelf/Dockerfile:
--------------------------------------------------------------------------------
1 | # Use the official Python image.
2 | # https://hub.docker.com/_/python
3 | FROM python:3.11-slim
4 |
5 | # Copy local code to the container image.
6 | ENV APP_HOME /app
7 | WORKDIR $APP_HOME
8 | COPY . ./
9 |
10 | # Install production dependencies.
11 | RUN pip install --no-cache-dir -r requirements.txt
12 |
13 | # Run the web service on container startup.
14 | ENTRYPOINT [ "gunicorn", "--bind", "0.0.0.0:8080", "main:app" ]
--------------------------------------------------------------------------------
/bookshelf/app.yaml:
--------------------------------------------------------------------------------
1 | runtime: python37
2 |
3 |
--------------------------------------------------------------------------------
/bookshelf/firestore.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | # [START bookshelf_firestore_client_import]
16 | from google.cloud import firestore
17 | # [END bookshelf_firestore_client_import]
18 |
19 |
20 | def document_to_dict(doc):
21 | if not doc.exists:
22 | return None
23 | doc_dict = doc.to_dict()
24 | doc_dict['id'] = doc.id
25 | return doc_dict
26 |
27 |
28 | def next_page(limit=10, start_after=None):
29 | db = firestore.Client()
30 |
31 | query = db.collection(u'Book').limit(limit).order_by(u'title')
32 |
33 | if start_after:
34 | # Construct a new query starting at this document.
35 | query = query.start_after({u'title': start_after})
36 |
37 | docs = query.stream()
38 | docs = list(map(document_to_dict, docs))
39 |
40 | last_title = None
41 | if limit == len(docs):
42 | # Get the last document from the results and set as the last title.
43 | last_title = docs[-1][u'title']
44 | return docs, last_title
45 |
46 |
47 | def read(book_id):
48 | # [START bookshelf_firestore_client]
49 | db = firestore.Client()
50 | book_ref = db.collection(u'Book').document(book_id)
51 | snapshot = book_ref.get()
52 | # [END bookshelf_firestore_client]
53 | return document_to_dict(snapshot)
54 |
55 |
56 | def update(data, book_id=None):
57 | db = firestore.Client()
58 | book_ref = db.collection(u'Book').document(book_id)
59 | book_ref.set(data)
60 | return document_to_dict(book_ref.get())
61 |
62 |
63 | create = update
64 |
65 |
66 | def delete(id):
67 | db = firestore.Client()
68 | book_ref = db.collection(u'Book').document(id)
69 | book_ref.delete()
70 |
--------------------------------------------------------------------------------
/bookshelf/images/moby-dick.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleCloudPlatform/getting-started-python/d9da8db203f7729f5da28b57be66b69084955bde/bookshelf/images/moby-dick.png
--------------------------------------------------------------------------------
/bookshelf/main.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import logging
16 |
17 | import firestore
18 | from flask import current_app, flash, Flask, Markup, redirect, render_template
19 | from flask import request, url_for
20 | from google.cloud import error_reporting
21 | import google.cloud.logging
22 | import storage
23 |
24 |
25 | # [START upload_image_file]
26 | def upload_image_file(img):
27 | """
28 | Upload the user-uploaded file to Google Cloud Storage and retrieve its
29 | publicly-accessible URL.
30 | """
31 | if not img:
32 | return None
33 |
34 | public_url = storage.upload_file(
35 | img.read(),
36 | img.filename,
37 | img.content_type
38 | )
39 |
40 | current_app.logger.info(
41 | 'Uploaded file %s as %s.', img.filename, public_url)
42 |
43 | return public_url
44 | # [END upload_image_file]
45 |
46 |
47 | app = Flask(__name__)
48 | app.config.update(
49 | SECRET_KEY='secret',
50 | MAX_CONTENT_LENGTH=8 * 1024 * 1024,
51 | ALLOWED_EXTENSIONS=set(['png', 'jpg', 'jpeg', 'gif'])
52 | )
53 |
54 | app.debug = False
55 | app.testing = False
56 |
57 | # Configure logging
58 | if not app.testing:
59 | logging.basicConfig(level=logging.INFO)
60 | client = google.cloud.logging.Client()
61 | # Attaches a Google Stackdriver logging handler to the root logger
62 | client.setup_logging()
63 |
64 |
65 | @app.route('/')
66 | def list():
67 | start_after = request.args.get('start_after', None)
68 | books, last_title = firestore.next_page(start_after=start_after)
69 |
70 | return render_template('list.html', books=books, last_title=last_title)
71 |
72 |
73 | @app.route('/books/')
74 | def view(book_id):
75 | book = firestore.read(book_id)
76 | return render_template('view.html', book=book)
77 |
78 |
79 | @app.route('/books/add', methods=['GET', 'POST'])
80 | def add():
81 | if request.method == 'POST':
82 | data = request.form.to_dict(flat=True)
83 |
84 | # If an image was uploaded, update the data to point to the new image.
85 | image_url = upload_image_file(request.files.get('image'))
86 |
87 | if image_url:
88 | data['imageUrl'] = image_url
89 |
90 | book = firestore.create(data)
91 |
92 | return redirect(url_for('.view', book_id=book['id']))
93 |
94 | return render_template('form.html', action='Add', book={})
95 |
96 |
97 | @app.route('/books//edit', methods=['GET', 'POST'])
98 | def edit(book_id):
99 | book = firestore.read(book_id)
100 |
101 | if request.method == 'POST':
102 | data = request.form.to_dict(flat=True)
103 |
104 | # If an image was uploaded, update the data to point to the new image.
105 | image_url = upload_image_file(request.files.get('image'))
106 |
107 | if image_url:
108 | data['imageUrl'] = image_url
109 |
110 | book = firestore.update(data, book_id)
111 |
112 | return redirect(url_for('.view', book_id=book['id']))
113 |
114 | return render_template('form.html', action='Edit', book=book)
115 |
116 |
117 | @app.route('/books//delete')
118 | def delete(book_id):
119 | firestore.delete(book_id)
120 | return redirect(url_for('.list'))
121 |
122 |
123 | @app.route('/logs')
124 | def logs():
125 | logging.info('Hey, you triggered a custom log entry. Good job!')
126 | flash(Markup('''You triggered a custom log entry. You can view it in the
127 | Cloud Console'''))
128 | return redirect(url_for('.list'))
129 |
130 |
131 | @app.route('/errors')
132 | def errors():
133 | raise Exception('This is an intentional exception.')
134 |
135 |
136 | # Add an error handler that reports exceptions to Stackdriver Error
137 | # Reporting. Note that this error handler is only used when debug
138 | # is False
139 | @app.errorhandler(500)
140 | def server_error(e):
141 | client = error_reporting.Client()
142 | client.report_exception(
143 | http_context=error_reporting.build_flask_context(request))
144 | return """
145 | An internal error occurred:
{}
146 | See logs for full stacktrace.
147 | """.format(e), 500
148 |
149 |
150 | # This is only used when running locally. When running live, gunicorn runs
151 | # the application.
152 | if __name__ == '__main__':
153 | app.run(host='127.0.0.1', port=8080, debug=True)
154 |
--------------------------------------------------------------------------------
/bookshelf/main_test.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import os
16 | import re
17 |
18 | import google.auth
19 | import main
20 | import pytest
21 | import requests
22 | from six import BytesIO
23 |
24 |
25 | credentials, project_id = google.auth.default()
26 | os.environ['GOOGLE_CLOUD_PROJECT'] = project_id
27 |
28 |
29 | @pytest.fixture
30 | def app(request):
31 | """This fixture provides a Flask app instance configured for testing.
32 |
33 | It also ensures the tests run within a request context, allowing
34 | any calls to flask.request, flask.current_app, etc. to work."""
35 | app = main.app
36 |
37 | with app.test_request_context():
38 | yield app
39 |
40 |
41 | @pytest.fixture
42 | def firestore():
43 | """This fixture provides a modified version of the app's Firebase model
44 | that tracks all created items and deletes them at the end of the test.
45 |
46 | Any tests that directly or indirectly interact with the database should
47 | use this to ensure that resources are properly cleaned up.
48 | """
49 |
50 | import firestore
51 |
52 | # Ensure no books exist before running the tests. This typically helps if
53 | # tests somehow left the database in a bad state.
54 | delete_all_books(firestore)
55 |
56 | yield firestore
57 |
58 | # Delete all books that we created during tests.
59 | delete_all_books(firestore)
60 |
61 |
62 | def delete_all_books(firestore):
63 | while True:
64 | books, _ = firestore.next_page(limit=50)
65 | if not books:
66 | break
67 | for book in books:
68 | firestore.delete(book['id'])
69 |
70 |
71 | def test_list(app, firestore):
72 | for i in range(1, 12):
73 | firestore.create({'title': u'Book {0}'.format(i)})
74 |
75 | with app.test_client() as c:
76 | rv = c.get('/')
77 |
78 | assert rv.status == '200 OK'
79 |
80 | body = rv.data.decode('utf-8')
81 | assert 'Book 1' in body, "Should show books"
82 | assert len(re.findall('
Book', body)) <= 10, (
83 | "Should not show more than 10 books")
84 | assert 'More' in body, "Should have more than one page"
85 |
86 |
87 | def test_add(app):
88 | data = {
89 | 'title': 'Test Book',
90 | 'author': 'Test Author',
91 | 'publishedDate': 'Test Date Published',
92 | 'description': 'Test Description'
93 | }
94 |
95 | with app.test_client() as c:
96 | rv = c.post('books/add', data=data, follow_redirects=True)
97 |
98 | assert rv.status == '200 OK'
99 | body = rv.data.decode('utf-8')
100 | assert 'Test Book' in body
101 | assert 'Test Author' in body
102 | assert 'Test Date Published' in body
103 | assert 'Test Description' in body
104 |
105 |
106 | def test_edit(app, firestore):
107 | existing = firestore.create({'title': "Temp Title"})
108 |
109 | with app.test_client() as c:
110 | rv = c.post(
111 | 'books/%s/edit' % existing['id'],
112 | data={'title': 'Updated Title'},
113 | follow_redirects=True)
114 |
115 | assert rv.status == '200 OK'
116 | body = rv.data.decode('utf-8')
117 | assert 'Updated Title' in body
118 | assert 'Temp Title' not in body
119 |
120 |
121 | def test_delete(app, firestore):
122 | existing = firestore.create({'title': "Temp Title"})
123 |
124 | with app.test_client() as c:
125 | rv = c.get(
126 | 'books/%s/delete' % existing['id'],
127 | follow_redirects=True)
128 |
129 | assert rv.status == '200 OK'
130 | assert not firestore.read(existing['id'])
131 |
132 |
133 | def test_upload_image(app):
134 | data = {
135 | 'title': 'Test Book',
136 | 'author': 'Test Author',
137 | 'publishedDate': 'Test Date Published',
138 | 'description': 'Test Description',
139 | 'image': (BytesIO(b'hello world'), 'hello.jpg')
140 | }
141 |
142 | with app.test_client() as c:
143 | rv = c.post('books/add', data=data, follow_redirects=True)
144 |
145 | assert rv.status == '200 OK'
146 | body = rv.data.decode('utf-8')
147 |
148 | img_tag = re.search(''),
162 | '1337h4x0r.php')
163 | }
164 |
165 | with app.test_client() as c:
166 | rv = c.post('/books/add', data=data, follow_redirects=True)
167 |
168 | # check we weren't pwned
169 | assert rv.status == '400 BAD REQUEST'
170 |
--------------------------------------------------------------------------------
/bookshelf/requirements.txt:
--------------------------------------------------------------------------------
1 | Flask==2.2.5
2 | google-cloud-firestore==2.11.1
3 | google-cloud-storage==2.9.0
4 | google-cloud-error-reporting==1.9.1
5 | google-cloud-logging==3.5.0
6 | gunicorn==20.1.0
7 | six==1.16.0
8 |
--------------------------------------------------------------------------------
/bookshelf/storage.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | from __future__ import absolute_import
16 |
17 | import datetime
18 | import os
19 |
20 | from flask import current_app
21 | from google.cloud import storage
22 | import six
23 | from werkzeug.exceptions import BadRequest
24 | from werkzeug.utils import secure_filename
25 |
26 |
27 | def _check_extension(filename, allowed_extensions):
28 | file, ext = os.path.splitext(filename)
29 | if (ext.replace('.', '') not in allowed_extensions):
30 | raise BadRequest(
31 | '{0} has an invalid name or extension'.format(filename))
32 |
33 |
34 | def _safe_filename(filename):
35 | """
36 | Generates a safe filename that is unlikely to collide with existing
37 | objects in Google Cloud Storage.
38 |
39 | ``filename.ext`` is transformed into ``filename-YYYY-MM-DD-HHMMSS.ext``
40 | """
41 | filename = secure_filename(filename)
42 | date = datetime.datetime.utcnow().strftime("%Y-%m-%d-%H%M%S")
43 | basename, extension = filename.rsplit('.', 1)
44 | return "{0}-{1}.{2}".format(basename, date, extension)
45 |
46 |
47 | def upload_file(file_stream, filename, content_type):
48 | """
49 | Uploads a file to a given Cloud Storage bucket and returns the public url
50 | to the new object.
51 | """
52 | _check_extension(filename, current_app.config['ALLOWED_EXTENSIONS'])
53 | filename = _safe_filename(filename)
54 |
55 | bucketname = os.getenv('GOOGLE_STORAGE_BUCKET') or os.getenv(
56 | 'GOOGLE_CLOUD_PROJECT') + '_bucket'
57 |
58 | # [START bookshelf_cloud_storage_client]
59 | client = storage.Client()
60 | bucket = client.bucket(bucketname)
61 | blob = bucket.blob(filename)
62 |
63 | blob.upload_from_string(
64 | file_stream,
65 | content_type=content_type)
66 | # Ensure the file is publicly readable.
67 | blob.make_public()
68 |
69 | url = blob.public_url
70 | # [END bookshelf_cloud_storage_client]
71 |
72 | if isinstance(url, six.binary_type):
73 | url = url.decode('utf-8')
74 |
75 | return url
76 |
--------------------------------------------------------------------------------
/bookshelf/templates/base.html:
--------------------------------------------------------------------------------
1 | {#
2 | # Copyright 2019 Google LLC
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 | #}
16 |
17 |
18 |
19 | Bookshelf - Python on Google Cloud Platform
20 |
21 |
22 |
23 |
24 |
25 |
38 | {{user}}
39 |
40 |
41 |
--------------------------------------------------------------------------------
/bookshelf/templates/form.html:
--------------------------------------------------------------------------------
1 | {#
2 | # Copyright 2019 Google LLC
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 | #}
16 |
17 | {# [START form] #}
18 | {% extends "base.html" %}
19 |
20 | {% block content %}
21 |
{{action}} book
22 |
23 |
57 |
58 | {% endblock %}
59 | {# [END form] #}
60 |
--------------------------------------------------------------------------------
/bookshelf/templates/list.html:
--------------------------------------------------------------------------------
1 | {#
2 | # Copyright 2019 Google LLC
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 | #}
16 |
17 | {% extends "base.html" %}
18 |
19 | {% block content %}
20 |
21 | {% with messages = get_flashed_messages() %}
22 | {% for message in messages %}
23 |
51 | {% endfor %}
52 |
53 | {% if last_title %}
54 |
59 | {% endif %}
60 |
61 | {% endblock %}
62 |
--------------------------------------------------------------------------------
/bookshelf/templates/view.html:
--------------------------------------------------------------------------------
1 | {#
2 | # Copyright 2019 Google LLC
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 | #}
16 |
17 | {% extends "base.html" %}
18 |
19 | {% block content %}
20 |
21 |
53 |
54 | {% endblock %}
55 |
--------------------------------------------------------------------------------
/decrypt-secrets.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Copyright 2016 Google Inc. All rights reserved.
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 |
17 | set -euo pipefail
18 |
19 | # Always cd to the project root.
20 | readonly root="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
21 | cd ${root}
22 |
23 | # Use SECRET_MANAGER_PROJECT if set, fallback to cloud-devrel-kokoro-resources.
24 | readonly project_id="${SECRET_MANAGER_PROJECT:-cloud-devrel-kokoro-resources}"
25 |
26 | # If there's already a secret file, skip retrieving the secret.
27 | if [[ -f "service-account.json" ]]; then
28 | echo "The secret already exists, skipping."
29 | exit 0
30 | fi
31 |
32 | gcloud secrets versions access latest \
33 | --secret="getting-started-python-service-account" \
34 | --project="${project_id}" \
35 | > service-account.json
36 |
--------------------------------------------------------------------------------
/encrypt-secrets.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Copyright 2016 Google Inc. All rights reserved.
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 |
17 | set -euo pipefail
18 |
19 | # Always cd to the project root.
20 | readonly root="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
21 | cd ${root}
22 |
23 | # Use SECRET_MANAGER_PROJECT if set, fallback to cloud-devrel-kokoro-resources.
24 | readonly project_id="${SECRET_MANAGER_PROJECT:-cloud-devrel-kokoro-resources}"
25 |
26 | gcloud secrets versions add "getting-started-python-service-account" \
27 | --project="${project_id}" \
28 | --data-file="service-account.json"
29 |
--------------------------------------------------------------------------------
/gce/README.md:
--------------------------------------------------------------------------------
1 | # Hello World for Python on Google Compute Engine
2 |
3 | This folder contains the sample code for the [Deploying to Google Compute Engine][tutorial-gce]
4 | tutorial. Please refer to the tutorial for instructions on configuring, running,
5 | and deploying this sample.
6 |
7 | [tutorial-gce]: https://cloud.google.com/python/tutorials/getting-started-on-compute-engine
8 |
--------------------------------------------------------------------------------
/gce/add-google-cloud-ops-agent-repo.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # Copyright 2020 Google Inc. All rights reserved.
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 | #
16 | # *NOTE*: The source of truth for this script is:
17 | # https://dl.google.com/cloudagents/add-google-cloud-ops-agent-repo.sh
18 | # See https://cloud.google.com/stackdriver/docs/solutions/agents/ops-agent/installation
19 | # for installation instructions.
20 | # It is committed to this repository to follow security best practices.
21 | #
22 | #
23 | # Add repository for the Google ops agent.
24 | #
25 | # This script adds the required apt or yum repository and installs or uninstalls
26 | # the agent based on the corresponding flags.
27 | #
28 | # Available flags:
29 | # * `--verbose`:
30 | # Turns on verbose logging during the script execution, which is helpful for
31 | # debugging purposes.
32 | #
33 | # * `--also-install`:
34 | # Installs the agent after adding the agent package repository. If this flag
35 | # is absent, the script only adds the agent package repository. This flag
36 | # can not be run with the `--uninstall` flag.
37 | #
38 | # * `--version `:
39 | # Sets the agent version for the script to install. Allowed formats:
40 | # * `latest`:
41 | # Adds an agent package repository that contains all agent versions, and
42 | # installs the latest version of the agent.
43 | # * `MAJOR_VERSION.*.*`:
44 | # Adds an agent package repository that contains all agent versions up to
45 | # this major version (e.g. `1.*.*`), and installs the latest version of
46 | # the agent within the range of that major version.
47 | # * `MAJOR_VERSION.MINOR_VERSION.PATCH_VERSION`:
48 | # Adds an agent package repository that contains all agent versions, and
49 | # installs the specified version of the agent (e.g. `3.2.1`).
50 | #
51 | # * `--uninstall`:
52 | # Uninstalls the agent. This flag can not be run with the `--also-install`
53 | # flag.
54 | #
55 | # * `--remove-repo`:
56 | # Removes the corresponding agent package repository after installing or
57 | # uninstalling the agent.
58 | #
59 | # * `--dry-run`:
60 | # Triggers only a dry run of the script execution and prints out the
61 | # commands that it is supposed to execute. This is helpful to know what
62 | # actions the script will take.
63 | #
64 | # * `--uninstall-standalone-logging-agent`:
65 | # Uninstalls the standalone logging agent (`google-fluentd`).
66 | #
67 | # * `--uninstall-standalone-monitoring-agent`:
68 | # Uninstalls the standalone monitoring agent (`stackdriver-agent`).
69 | #
70 | # Sample usage:
71 | # * To add the repo that contains all agent versions, run:
72 | # $ bash add-google-cloud-ops-agent-repo.sh
73 | #
74 | # * To add the repo and also install the agent, run:
75 | # $ bash add-google-cloud-ops-agent-repo.sh --also-install --version=
76 | #
77 | # * To uninstall the agent run:
78 | # $ bash add-google-cloud-ops-agent-repo.sh --uninstall
79 | #
80 | # * To uninstall the agent and remove the repo, run:
81 | # $ bash add-google-cloud-ops-agent-repo.sh --uninstall --remove-repo
82 | #
83 | # * To run the script with verbose logging, run:
84 | # $ bash add-google-cloud-ops-agent-repo.sh --also-install --verbose
85 | #
86 | # * To run the script in dry-run mode, run:
87 | # $ bash add-google-cloud-ops-agent-repo.sh --also-install --dry-run
88 | #
89 | # * To replace standalone agents with the Ops agent, run:
90 | # $ bash add-google-cloud-ops-agent-repo.sh --also-install --uninstall-standalone-logging-agent --uninstall-standalone-monitoring-agent
91 | #
92 | # Internal usage only:
93 | # The environment variable `REPO_SUFFIX` can be set to alter which repository is
94 | # used. A dash (-) will be inserted prior to the supplied suffix. `REPO_SUFFIX`
95 | # defaults to `all` which contains all agent versions across different major
96 | # versions. The full repository name is:
97 | # "google-cloud-ops-agent-[-]-".
98 |
99 | # Ignore the return code of command substitution in variables.
100 | # shellcheck disable=SC2155
101 | #
102 | # Initialize var used to notify config management tools of when a change is made.
103 | CHANGED=0
104 |
105 | fail() {
106 | echo >&2 "[$(date +'%Y-%m-%dT%H:%M:%S%z')] $*"
107 | exit 1
108 | }
109 |
110 | # Parsing flag value.
111 | declare -a ACTIONS=()
112 | DRY_RUN=''
113 | VERBOSE='false'
114 | while getopts -- '-:' OPTCHAR; do
115 | case "${OPTCHAR}" in
116 | -)
117 | case "${OPTARG}" in
118 | # Note: Do not remove entries from this list when deprecating flags.
119 | # That would break user scripts that specify those flags. Instead,
120 | # leave the flag in place but make it a noop.
121 | also-install) ACTIONS+=('also-install') ;;
122 | version=*) AGENT_VERSION="${OPTARG#*=}" ;;
123 | uninstall) ACTIONS+=('uninstall') ;;
124 | remove-repo) ACTIONS+=('remove-repo') ;;
125 | uninstall-standalone-logging-agent) ACTIONS+=('uninstall-standalone-logging-agent') ;;
126 | uninstall-standalone-monitoring-agent) ACTIONS+=('uninstall-standalone-monitoring-agent') ;;
127 | dry-run) echo 'Starting dry run'; DRY_RUN='dryrun' ;;
128 | verbose) VERBOSE='true' ;;
129 | *) fail "Unknown option '${OPTARG}'." ;;
130 | esac
131 | esac
132 | done
133 | [[ " ${ACTIONS[*]} " == *\ uninstall\ * || ( " ${ACTIONS[*]} " == *\ remove-repo\ * && " ${ACTIONS[*]} " != *\ also-install\ * )]] || \
134 | ACTIONS+=('add-repo')
135 | # Sort the actions array for easier parsing.
136 | readarray -t ACTIONS < <(printf '%s\n' "${ACTIONS[@]}" | sort)
137 | readonly ACTIONS DRY_RUN VERBOSE
138 |
139 | if [[ " ${ACTIONS[*]} " == *\ also-install*uninstall\ * ]]; then
140 | fail "Received conflicting flags 'also-install' and 'uninstall'."
141 | fi
142 |
143 | if [[ "${VERBOSE}" == 'true' ]]; then
144 | echo 'Enable verbose logging.'
145 | set -x
146 | fi
147 |
148 | # Host that serves the repositories.
149 | REPO_HOST='packages.cloud.google.com'
150 |
151 | # URL for the ops agent documentation.
152 | AGENT_DOCS_URL='https://cloud.google.com/stackdriver/docs/solutions/ops-agent'
153 |
154 | # URL documentation which lists supported platforms for running the ops agent.
155 | AGENT_SUPPORTED_URL="${AGENT_DOCS_URL}/#supported_operating_systems"
156 |
157 | # Packages to install.
158 | AGENT_PACKAGE='google-cloud-ops-agent'
159 | declare -a ADDITIONAL_PACKAGES=()
160 |
161 | if [[ -f /etc/os-release ]]; then
162 | . /etc/os-release
163 | fi
164 |
165 | # If dry-run mode is enabled, echo VM state-changing commands instead of executing them.
166 | dryrun() {
167 | # Needed for commands that use pipes.
168 | if [[ ! -t 0 ]]; then
169 | cat
170 | fi
171 | printf -v cmd_str '%q ' "$@"
172 | echo "DRY_RUN: Not executing '$cmd_str'"
173 | }
174 |
175 | refresh_failed() {
176 | local REPO_TYPE="$1"
177 | local OS_FAMILY="$2"
178 | fail "Could not refresh the google-cloud-ops-agent ${REPO_TYPE} repositories.
179 | Please check your network connectivity and make sure you are running a supported
180 | ${OS_FAMILY} distribution. See ${AGENT_SUPPORTED_URL}
181 | for a list of supported platforms."
182 | }
183 |
184 | resolve_version() {
185 | if [[ "${AGENT_VERSION:-latest}" == 'latest' ]]; then
186 | AGENT_VERSION=''
187 | elif grep -qE '^[0-9]+\.\*\.\*$' <<<"${AGENT_VERSION}"; then
188 | REPO_SUFFIX="${REPO_SUFFIX:-"${AGENT_VERSION%%.*}"}"
189 | elif ! grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$' <<<"${AGENT_VERSION}"; then
190 | fail "The agent version [${AGENT_VERSION}] is not allowed. Expected values: [latest],
191 | or anything in the format of [MAJOR_VERSION.MINOR_VERSION.PATCH_VERSION] or [MAJOR_VERSION.*.*]."
192 | fi
193 | }
194 |
195 | handle_debian() {
196 | declare -a EXTRA_OPTS=()
197 | [[ "${VERBOSE}" == 'true' ]] && EXTRA_OPTS+=(-oDebug::pkgAcquire::Worker=1)
198 |
199 | add_repo() {
200 | [[ -n "${REPO_CODENAME:-}" ]] || lsb_release -v >/dev/null 2>&1 || { \
201 | apt-get update; apt-get -y install lsb-release; CHANGED=1;
202 | }
203 | [[ "$(dpkg -l apt-transport-https 2>&1 | grep -o '^[a-z][a-z]')" == 'ii' ]] || { \
204 | ${DRY_RUN} apt-get update; ${DRY_RUN} apt-get -y install apt-transport-https; CHANGED=1;
205 | }
206 | [[ "$(dpkg -l ca-certificates 2>&1 | grep -o '^[a-z][a-z]')" == 'ii' ]] || { \
207 | ${DRY_RUN} apt-get update; ${DRY_RUN} apt-get -y install ca-certificates; CHANGED=1;
208 | }
209 | local CODENAME="${REPO_CODENAME:-"$(lsb_release -sc)"}"
210 | local REPO_NAME="google-cloud-ops-agent-${CODENAME}-${REPO_SUFFIX:-all}"
211 | local REPO_DATA="deb https://${REPO_HOST}/apt ${REPO_NAME} main"
212 | if ! cmp -s <<<"${REPO_DATA}" - /etc/apt/sources.list.d/google-cloud-ops-agent.list; then
213 | echo "Adding agent repository for ${ID}."
214 | ${DRY_RUN} tee <<<"${REPO_DATA}" /etc/apt/sources.list.d/google-cloud-ops-agent.list
215 | ${DRY_RUN} curl --connect-timeout 5 -s -f "https://${REPO_HOST}/apt/doc/apt-key.gpg" \
216 | | ${DRY_RUN} apt-key add -
217 | CHANGED=1
218 | fi
219 | }
220 |
221 | remove_repo() {
222 | if [[ -f /etc/apt/sources.list.d/google-cloud-ops-agent.list ]]; then
223 | echo "Removing agent repository for ${ID}."
224 | ${DRY_RUN} rm /etc/apt/sources.list.d/google-cloud-ops-agent.list
225 | CHANGED=1
226 | fi
227 | }
228 |
229 | expected_version_installed() {
230 | [[ "$(dpkg -l "${AGENT_PACKAGE}" "${ADDITIONAL_PACKAGES[@]}" 2>&1 | grep -o '^[a-z][a-z]' | sort -u)" == 'ii' ]] || \
231 | return
232 | if [[ -z "${AGENT_VERSION:-}" ]]; then
233 | apt-get --dry-run install "${AGENT_PACKAGE}" "${ADDITIONAL_PACKAGES[@]}" \
234 | | grep -qo '^0 upgraded, 0 newly installed'
235 | elif grep -qE '^[0-9]+\.\*\.\*$' <<<"${AGENT_VERSION}"; then
236 | dpkg -l "${AGENT_PACKAGE}" | grep -qE "$AGENT_PACKAGE $AGENT_VERSION" && \
237 | apt-get --dry-run install "${AGENT_PACKAGE}" "${ADDITIONAL_PACKAGES[@]}" \
238 | | grep -qo '^0 upgraded, 0 newly installed'
239 | else
240 | dpkg -l "${AGENT_PACKAGE}" | grep -qE "$AGENT_PACKAGE $AGENT_VERSION"
241 | fi
242 | }
243 |
244 | install_agent() {
245 | ${DRY_RUN} apt-get update || refresh_failed 'apt' "${ID}"
246 | expected_version_installed || { \
247 | if [[ -n "${AGENT_VERSION:-}" ]]; then
248 | # Differentiate `MAJOR_VERSION.MINOR_VERSION.PATCH_VERSION` from `MAJOR_VERSION.*.*`.
249 | # apt package version format: e.g. 2.0.1~debian9.13.
250 | if grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$' <<<"${AGENT_VERSION}"; then
251 | AGENT_VERSION="=${AGENT_VERSION}~*"
252 | else
253 | AGENT_VERSION="=${AGENT_VERSION%.\*}"
254 | fi
255 | fi
256 | ${DRY_RUN} apt-get -y --allow-downgrades "${EXTRA_OPTS[@]}" install "${AGENT_PACKAGE}${AGENT_VERSION}" \
257 | "${ADDITIONAL_PACKAGES[@]}" || fail "${AGENT_PACKAGE} ${ADDITIONAL_PACKAGES[*]} \
258 | installation failed."
259 | echo "${AGENT_PACKAGE} ${ADDITIONAL_PACKAGES[*]} installation succeeded."
260 | CHANGED=1
261 | }
262 | }
263 |
264 | uninstall() {
265 | local -a packages=("$@")
266 | # Return early unless at least one package is installed.
267 | dpkg -l "${packages[@]}" 2>&1 | grep -qo '^ii' || return
268 | ${DRY_RUN} apt-get -y "${EXTRA_OPTS[@]}" remove "${packages[@]}" || \
269 | fail "${packages[*]} uninstallation failed."
270 | echo "${packages[*]} uninstallation succeeded."
271 | CHANGED=1
272 | }
273 | }
274 |
275 | handle_rpm() {
276 | declare -a EXTRA_OPTS=()
277 | [[ "${VERBOSE}" == 'true' ]] && EXTRA_OPTS+=(-v)
278 |
279 | add_repo() {
280 | local REPO_NAME="google-cloud-ops-agent-${CODENAME}-\$basearch-${REPO_SUFFIX:-all}"
281 | local REPO_DATA="\
282 | [google-cloud-ops-agent]
283 | name=Google Cloud Ops Agent Repository
284 | baseurl=https://${REPO_HOST}/yum/repos/${REPO_NAME}
285 | autorefresh=0
286 | enabled=1
287 | type=rpm-md
288 | gpgcheck=1
289 | repo_gpgcheck=0
290 | gpgkey=https://${REPO_HOST}/yum/doc/yum-key.gpg
291 | https://${REPO_HOST}/yum/doc/rpm-package-key.gpg"
292 | if ! cmp -s <<<"${REPO_DATA}" - /etc/yum.repos.d/google-cloud-ops-agent.repo; then
293 | echo "Adding agent repository for ${ID}."
294 | ${DRY_RUN} tee <<<"${REPO_DATA}" /etc/yum.repos.d/google-cloud-ops-agent.repo
295 | # After repo upgrades, CentOS7/RHEL7 won't pick up newly available packages
296 | # until the cache is cleared.
297 | ${DRY_RUN} rm -rf /var/cache/yum/*/*/google-cloud-ops-agent/
298 | CHANGED=1
299 | fi
300 | }
301 |
302 | remove_repo() {
303 | if [[ -f /etc/yum.repos.d/google-cloud-ops-agent.repo ]]; then
304 | echo "Removing agent repository for ${ID}."
305 | ${DRY_RUN} rm /etc/yum.repos.d/google-cloud-ops-agent.repo
306 | CHANGED=1
307 | fi
308 | }
309 |
310 | expected_version_installed() {
311 | rpm -q "${AGENT_PACKAGE}" "${ADDITIONAL_PACKAGES[@]}" >/dev/null 2>&1 || return
312 | if [[ -z "${AGENT_VERSION:-}" ]]; then
313 | yum -y check-update "${AGENT_PACKAGE}" "${ADDITIONAL_PACKAGES[@]}" >/dev/null 2>&1
314 | elif grep -qE '^[0-9]+\.\*\.\*$' <<<"${AGENT_VERSION}"; then
315 | CURRENT_VERSION="$(rpm -q --queryformat '%{VERSION}' "${AGENT_PACKAGE}")"
316 | grep -qE "${AGENT_VERSION}" <<<"${CURRENT_VERSION}" && \
317 | yum -y check-update "${AGENT_PACKAGE}" "${ADDITIONAL_PACKAGES[@]}" >/dev/null 2>&1
318 | else
319 | CURRENT_VERSION="$(rpm -q --queryformat '%{VERSION}' "${AGENT_PACKAGE}")"
320 | [[ "${AGENT_VERSION}" == "${CURRENT_VERSION}" ]]
321 | fi
322 | }
323 |
324 | install_agent() {
325 | expected_version_installed || { \
326 | ${DRY_RUN} yum -y list updates || refresh_failed 'yum' "${ID}"
327 | local COMMAND='install'
328 | if [[ -n "${AGENT_VERSION:-}" ]]; then
329 | [[ -z "${CURRENT_VERSION:-}" ]] || \
330 | [[ "${AGENT_VERSION}" == "$(sort -rV <<<"${AGENT_VERSION}"$'\n'"${CURRENT_VERSION}" | head -1)" ]] || \
331 | COMMAND='downgrade'
332 | # Differentiate `MAJOR_VERSION.MINOR_VERSION.PATCH_VERSION` from `MAJOR_VERSION.*.*`.
333 | # yum package version format: e.g. 1.0.1-1.el8.
334 | if grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$' <<<"${AGENT_VERSION}"; then
335 | AGENT_VERSION="-${AGENT_VERSION}-1*"
336 | else
337 | AGENT_VERSION="-${AGENT_VERSION}"
338 | fi
339 | fi
340 | ${DRY_RUN} yum -y "${EXTRA_OPTS[@]}" "${COMMAND}" "${AGENT_PACKAGE}${AGENT_VERSION}" \
341 | "${ADDITIONAL_PACKAGES[@]}" || fail "${AGENT_PACKAGE} ${ADDITIONAL_PACKAGES[*]} \
342 | installation failed."
343 | echo "${AGENT_PACKAGE} ${ADDITIONAL_PACKAGES[*]} installation succeeded."
344 | CHANGED=1
345 | }
346 | }
347 |
348 | uninstall() {
349 | local -a packages=("$@")
350 | # Return early if none of the packages are installed.
351 | rpm -q "${packages[@]}" | grep -qvE 'is not installed$' || return
352 | ${DRY_RUN} yum -y "${EXTRA_OPTS[@]}" remove "${packages[@]}" || \
353 | fail "${packages[*]} uninstallation failed."
354 | echo "${packages[*]} uninstallation succeeded."
355 | CHANGED=1
356 | }
357 | }
358 |
359 | handle_redhat() {
360 | local MAJOR_VERSION="$(rpm --eval %{?rhel})"
361 | CODENAME="el${MAJOR_VERSION}"
362 | handle_rpm
363 | }
364 |
365 | handle_suse() {
366 | declare -a EXTRA_OPTS=()
367 | [[ "${VERBOSE}" == 'true' ]] && EXTRA_OPTS+=(-vv)
368 |
369 | add_repo() {
370 | local SUSE_VERSION=${VERSION_ID%%.*}
371 | local CODENAME="sles${SUSE_VERSION}"
372 | local REPO_NAME="google-cloud-ops-agent-${CODENAME}-\$basearch-${REPO_SUFFIX:-all}"
373 | {
374 | ${DRY_RUN} zypper --non-interactive refresh || { \
375 | echo >&2 'Could not refresh zypper repositories.'; \
376 | echo >&2 'This is not necessarily a fatal error; proceeding...'; \
377 | }
378 | } | grep -qF 'Retrieving repository' || [[ -n "${DRY_RUN:-}" ]] && CHANGED=1
379 | local REPO_DATA="\
380 | [google-cloud-ops-agent]
381 | name=Google Cloud Ops Agent Repository
382 | baseurl=https://${REPO_HOST}/yum/repos/${REPO_NAME}
383 | autorefresh=0
384 | enabled=1
385 | type=rpm-md
386 | gpgkey=https://${REPO_HOST}/yum/doc/yum-key.gpg
387 | https://${REPO_HOST}/yum/doc/rpm-package-key.gpg"
388 | if ! cmp -s <<<"${REPO_DATA}" - /etc/zypp/repos.d/google-cloud-ops-agent.repo; then
389 | echo "Adding agent repository for ${ID}."
390 | ${DRY_RUN} tee <<<"${REPO_DATA}" /etc/zypp/repos.d/google-cloud-ops-agent.repo
391 | CHANGED=1
392 | fi
393 | local RPM_KEYS="$(rpm --query gpg-pubkey)" # Save the installed keys.
394 | ${DRY_RUN} rpm --import "https://${REPO_HOST}/yum/doc/yum-key.gpg" "https://${REPO_HOST}/yum/doc/rpm-package-key.gpg"
395 | if [[ -n "${DRY_RUN:-}" ]] || ! cmp --silent <<<"${RPM_KEYS}" - <(rpm --query gpg-pubkey); then
396 | CHANGED=1
397 | fi
398 | {
399 | ${DRY_RUN} zypper --non-interactive --gpg-auto-import-keys refresh google-cloud-ops-agent || \
400 | refresh_failed 'zypper' "${ID}"; \
401 | } | grep -qF 'Retrieving repository' || [[ -n "${DRY_RUN:-}" ]] && CHANGED=1
402 | }
403 |
404 | remove_repo() {
405 | if [[ -f /etc/zypp/repos.d/google-cloud-ops-agent.repo ]]; then
406 | echo "Removing agent repository for ${ID}."
407 | ${DRY_RUN} rm /etc/zypp/repos.d/google-cloud-ops-agent.repo
408 | CHANGED=1
409 | fi
410 | }
411 |
412 | expected_version_installed() {
413 | rpm -q "${AGENT_PACKAGE}" "${ADDITIONAL_PACKAGES[@]}" >/dev/null 2>&1 || return
414 | if [[ -z "${AGENT_VERSION:-}" ]]; then
415 | zypper --non-interactive update --dry-run "${AGENT_PACKAGE}" "${ADDITIONAL_PACKAGES[@]}" \
416 | | grep -qE '^Nothing to do.'
417 | elif grep -qE '^[0-9]+\.\*\.\*$' <<<"${AGENT_VERSION}"; then
418 | rpm -q --queryformat '%{VERSION}' "${AGENT_PACKAGE}" | grep -qE "${AGENT_VERSION}" && \
419 | zypper --non-interactive update --dry-run "${AGENT_PACKAGE}" "${ADDITIONAL_PACKAGES[@]}" \
420 | | grep -qE '^Nothing to do.'
421 | else
422 | [[ "${AGENT_VERSION}" == "$(rpm -q --queryformat '%{VERSION}' "${AGENT_PACKAGE}")" ]]
423 | fi
424 | }
425 |
426 | install_agent() {
427 | expected_version_installed || { \
428 | if [[ -n "${AGENT_VERSION:-}" ]]; then
429 | # Differentiate `MAJOR_VERSION.MINOR_VERSION.PATCH_VERSION` from `MAJOR_VERSION.*.*`.
430 | # zypper package version format: e.g. 1.0.6-1.sles15.
431 | if grep -qE '^[0-9]+\.\*\.\*$' <<<"${AGENT_VERSION}"; then
432 | AGENT_VERSION="<$(( ${AGENT_VERSION%%.*} + 1 ))"
433 | else
434 | AGENT_VERSION="=${AGENT_VERSION}"
435 | fi
436 | fi
437 | ${DRY_RUN} zypper --non-interactive "${EXTRA_OPTS[@]}" install --oldpackage "${AGENT_PACKAGE}${AGENT_VERSION}" \
438 | "${ADDITIONAL_PACKAGES[@]}" || fail "${AGENT_PACKAGE} ${ADDITIONAL_PACKAGES[*]} \
439 | installation failed."
440 | echo "${AGENT_PACKAGE} ${ADDITIONAL_PACKAGES[*]} installation succeeded."
441 | CHANGED=1
442 | }
443 | }
444 |
445 | uninstall() {
446 | local -a packages=("$@")
447 | # Return early if none of the packages are installed.
448 | rpm -q "${packages[@]}" | grep -qvE 'is not installed$' || return
449 | ${DRY_RUN} zypper --non-interactive "${EXTRA_OPTS[@]}" remove "${packages[@]}" || \
450 | fail "${packages[*]} uninstallation failed."
451 | echo "${packages[*]} uninstallation succeeded."
452 | CHANGED=1
453 | }
454 | }
455 |
456 | save_configuration_files() {
457 | local save_dir="/var/lib/google-cloud-ops-agent/saved_configs"
458 | ${DRY_RUN} mkdir -p "${save_dir}"
459 | ${DRY_RUN} cp -rp "$@" "${save_dir}"
460 | echo "$* is now copied over to ${save_dir} folder."
461 | }
462 |
463 | main() {
464 | case "${ID:-}" in
465 | debian|ubuntu) handle_debian ;;
466 | rhel|centos) handle_redhat ;;
467 | sles|opensuse-leap) handle_suse ;;
468 | *)
469 | # Fallback for systems lacking /etc/os-release.
470 | if [[ -f /etc/debian_version ]]; then
471 | ID='debian'
472 | handle_debian
473 | elif [[ -f /etc/redhat-release ]]; then
474 | ID='rhel'
475 | handle_redhat
476 | elif [[ -f /etc/SuSE-release ]]; then
477 | ID='sles'
478 | handle_suse
479 | else
480 | fail "Unidentifiable or unsupported platform. See
481 | ${AGENT_SUPPORTED_URL} for a list of supported platforms."
482 | fi
483 | esac
484 |
485 |
486 | if [[ " ${ACTIONS[*]} " == *\ uninstall-standalone-logging-agent\ * ]]; then
487 | save_configuration_files "/etc/google-fluentd"
488 | # This will also remove dependent packages, e.g. "google-fluentd-catch-all-config" or "google-fluentd-catch-all-config-structured".
489 | uninstall "google-fluentd"
490 | fi
491 | if [[ " ${ACTIONS[*]} " == *\ uninstall-standalone-monitoring-agent\ * ]]; then
492 | save_configuration_files "/etc/stackdriver" "/opt/stackdriver/collectd/etc"
493 | uninstall "stackdriver-agent"
494 | fi
495 | if [[ " ${ACTIONS[*]} " == *\ add-repo\ * ]]; then
496 | resolve_version
497 | add_repo
498 | fi
499 | if [[ " ${ACTIONS[*]} " == *\ also-install\ * ]]; then
500 | install_agent
501 | elif [[ " ${ACTIONS[*]} " == *\ uninstall\ * ]]; then
502 | save_configuration_files "/etc/google-cloud-ops-agent"
503 | uninstall "${AGENT_PACKAGE}" "${ADDITIONAL_PACKAGES[@]}"
504 | fi
505 | if [[ " ${ACTIONS[*]} " == *\ remove-repo\ * ]]; then
506 | remove_repo
507 | fi
508 |
509 | if [[ "${CHANGED}" == 0 ]]; then
510 | echo 'No changes made.'
511 | fi
512 |
513 | if [[ -n "${DRY_RUN:-}" ]]; then
514 | echo 'Finished dry run. This was only a simulation, remove the --dry-run flag
515 | to perform an actual execution of the script.'
516 | fi
517 | }
518 |
519 | main "$@"
520 |
--------------------------------------------------------------------------------
/gce/deploy.sh:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Google LLC All Rights Reserved.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | set -ex
16 |
17 | # [START getting_started_gce_create_instance]
18 | MY_INSTANCE_NAME="my-app-instance"
19 | ZONE=us-central1-a
20 |
21 | gcloud compute instances create $MY_INSTANCE_NAME \
22 | --image-family=debian-10 \
23 | --image-project=debian-cloud \
24 | --machine-type=g1-small \
25 | --scopes userinfo-email,cloud-platform \
26 | --metadata-from-file startup-script=startup-script.sh \
27 | --zone $ZONE \
28 | --tags http-server
29 | # [END getting_started_gce_create_instance]
30 |
31 | gcloud compute firewall-rules create default-allow-http-8080 \
32 | --allow tcp:8080 \
33 | --source-ranges 0.0.0.0/0 \
34 | --target-tags http-server \
35 | --description "Allow port 8080 access to http-server"
36 |
--------------------------------------------------------------------------------
/gce/main.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Google LLC All Rights Reserved.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | from flask import Flask
16 | app = Flask(__name__)
17 |
18 |
19 | @app.route('/', methods=['GET'])
20 | def say_hello():
21 | return "Hello, world!"
22 |
23 |
24 | if __name__ == '__main__':
25 | app.run(host='127.0.0.1', port=8080, debug=True)
26 |
--------------------------------------------------------------------------------
/gce/main_test.py:
--------------------------------------------------------------------------------
1 | # Copyright 2022 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import main
16 |
17 |
18 | def test_hello():
19 | main.app.testing = True
20 | client = main.app.test_client()
21 |
22 | r = client.get("/")
23 | assert r.status_code == 200
24 | response_text = r.data.decode("utf-8")
25 | assert "Hello, world!" in response_text
26 |
--------------------------------------------------------------------------------
/gce/procfile:
--------------------------------------------------------------------------------
1 | hello: /opt/app/gce/env/bin/gunicorn -b 0.0.0.0:8080 main:app
2 |
--------------------------------------------------------------------------------
/gce/python-app.conf:
--------------------------------------------------------------------------------
1 | [program:pythonapp]
2 | directory=/opt/app/gce
3 | command=/opt/app/gce/env/bin/honcho start -f ./procfile hello
4 | autostart=true
5 | autorestart=true
6 | user=pythonapp
7 | # Environment variables ensure that the application runs inside of the
8 | # configured virtualenv.
9 | environment=VIRTUAL_ENV="/opt/app/gce/env",PATH="/opt/app/gce/env/bin",HOME="/home/pythonapp",USER="pythonapp"
10 | stdout_logfile=syslog
11 | stderr_logfile=syslog
12 |
--------------------------------------------------------------------------------
/gce/requirements.txt:
--------------------------------------------------------------------------------
1 | flask==2.2.5
2 | honcho==1.1.0
3 | gunicorn==20.1.0
4 |
--------------------------------------------------------------------------------
/gce/startup-script.sh:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Google LLC All Rights Reserved.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | # Echo commands and fail on error
16 | set -ev
17 |
18 | # [START getting_started_gce_startup_script]
19 | # Install or update needed software
20 | apt-get update
21 | apt-get install -yq git supervisor python python-pip python3-distutils
22 | pip install --upgrade pip virtualenv
23 |
24 | # Fetch source code
25 | export HOME=/root
26 | git clone https://github.com/GoogleCloudPlatform/getting-started-python.git /opt/app
27 |
28 | # Install Cloud Ops Agent
29 | sudo bash /opt/app/gce/add-google-cloud-ops-agent-repo.sh --also-install
30 |
31 | # Account to own server process
32 | useradd -m -d /home/pythonapp pythonapp
33 |
34 | # Python environment setup
35 | virtualenv -p python3 /opt/app/gce/env
36 | /bin/bash -c "source /opt/app/gce/env/bin/activate"
37 | /opt/app/gce/env/bin/pip install -r /opt/app/gce/requirements.txt
38 |
39 | # Set ownership to newly created account
40 | chown -R pythonapp:pythonapp /opt/app
41 |
42 | # Put supervisor configuration in proper place
43 | cp /opt/app/gce/python-app.conf /etc/supervisor/conf.d/python-app.conf
44 |
45 | # Start service via supervisorctl
46 | supervisorctl reread
47 | supervisorctl update
48 | # [END getting_started_gce_startup_script]
49 |
--------------------------------------------------------------------------------
/gce/teardown.sh:
--------------------------------------------------------------------------------
1 | #! /bin/bash
2 |
3 | # Copyright 2019 Google LLC All Rights Reserved.
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 |
17 | set -x
18 |
19 | MY_INSTANCE_NAME="my-app-instance"
20 | ZONE=us-central1-a
21 |
22 | gcloud compute instances delete $MY_INSTANCE_NAME \
23 | --zone=$ZONE --delete-disks=all
24 |
25 | gcloud compute firewall-rules delete default-allow-http-8080
26 |
--------------------------------------------------------------------------------
/noxfile.py:
--------------------------------------------------------------------------------
1 | from glob import glob
2 | import os
3 |
4 | import nox
5 |
6 | REPO_TOOLS_REQ = \
7 | 'git+https://github.com/GoogleCloudPlatform/python-repo-tools.git'
8 |
9 | DIRS = [
10 | 'authenticating-users',
11 | 'background/app',
12 | 'background/function',
13 | 'gce',
14 | 'sessions',
15 | 'bookshelf',
16 | ]
17 |
18 | PYTEST_COMMON_ARGS = ['--junitxml=sponge_log.xml', '-m', 'not e2e']
19 |
20 |
21 | @nox.session
22 | def check_requirements(session):
23 | session.install(REPO_TOOLS_REQ)
24 |
25 | if 'update' in session.posargs:
26 | command = 'update-requirements'
27 | else:
28 | command = 'check-requirements'
29 |
30 | for reqfile in glob('**/requirements*.txt'):
31 | session.run('gcp-devrel-py-tools', command, reqfile)
32 |
33 |
34 | @nox.session
35 | def lint(session):
36 | session.install('flake8', 'flake8-import-order')
37 | session.run(
38 | 'flake8', '--exclude=env,.nox,._config.py,.tox',
39 | '--import-order-style=google', '.')
40 |
41 |
42 | def run_test(session, dir):
43 | session.install('-r', 'requirements.txt')
44 | session.chdir(dir)
45 | if os.path.exists('requirements.txt'):
46 | session.install('-r', 'requirements.txt')
47 |
48 | session.env['PYTHONPATH'] = os.getcwd()
49 | session.run(
50 | 'pytest',
51 | *(PYTEST_COMMON_ARGS + session.posargs),
52 | # Pytest will return 5 when no tests are collected. This can happen
53 | # when slow and flaky tests are excluded.
54 | # See http://doc.pytest.org/en/latest/_modules/_pytest/main.html
55 | success_codes=[0, 5])
56 |
57 |
58 | @nox.session(python="3.12")
59 | @nox.parametrize('dir', DIRS)
60 | def run_tests(session, dir=None):
61 | """Run all tests for all directories (slow!)"""
62 | run_test(session, dir)
63 |
--------------------------------------------------------------------------------
/optional-kubernetes-engine/.dockerignore:
--------------------------------------------------------------------------------
1 | __pycache__
2 | *.pyc
3 | *.pyo
4 | *.pyd
5 | .Python
6 | env
7 | pip-log.txt
8 | pip-delete-this-directory.txt
9 | .tox
10 | .coverage
11 | .coverage.*
12 | .cache
13 | nosetests.xml
14 | coverage.xml
15 | *.cover
16 | *.log
17 | .git
18 | .mypy_cache
19 | .pytest_cache
20 | .hypothesis
21 |
--------------------------------------------------------------------------------
/optional-kubernetes-engine/Dockerfile:
--------------------------------------------------------------------------------
1 | # The Google App Engine python runtime is Debian Jessie with Python installed
2 | # and various os-level packages to allow installation of popular Python
3 | # libraries. The source is on github at:
4 | # https://github.com/GoogleCloudPlatform/python-docker
5 | FROM gcr.io/google-appengine/python
6 |
7 | # Create a virtualenv for the application dependencies.
8 | # If you want to use Python 2, add the -p python2.7 flag.
9 | RUN virtualenv -p python3.4 /env
10 |
11 | # Set virtualenv environment variables. This is equivalent to running
12 | # source /env/bin/activate. This ensures the application is executed within
13 | # the context of the virtualenv and will have access to its dependencies.
14 | ENV VIRTUAL_ENV /env
15 | ENV PATH /env/bin:$PATH
16 |
17 | # Install dependencies.
18 | ADD requirements.txt /app/requirements.txt
19 | RUN pip install -r /app/requirements.txt
20 |
21 | # Add application code.
22 | ADD . /app
23 |
24 | # Instead of using gunicorn directly, we'll use Honcho. Honcho is a python port
25 | # of the Foreman process manager. $PROCESSES is set in the pod manifest
26 | # to control which processes Honcho will start.
27 | CMD honcho start -f /app/procfile $PROCESSES
28 |
--------------------------------------------------------------------------------
/optional-kubernetes-engine/Makefile:
--------------------------------------------------------------------------------
1 | GCLOUD_PROJECT:=$(shell gcloud config list project --format="value(core.project)")
2 |
3 | .PHONY: all
4 | all: deploy
5 |
6 | .PHONY: create-cluster
7 | create-cluster:
8 | gcloud container clusters create bookshelf \
9 | --scopes "cloud-platform" \
10 | --num-nodes 2
11 | gcloud container clusters get-credentials bookshelf
12 |
13 | .PHONY: create-bucket
14 | create-bucket:
15 | gsutil mb gs://$(GCLOUD_PROJECT)
16 | gsutil defacl set public-read gs://$(GCLOUD_PROJECT)
17 |
18 | .PHONY: build
19 | build:
20 | docker build -t gcr.io/$(GCLOUD_PROJECT)/bookshelf .
21 |
22 | .PHONY: push
23 | push: build
24 | gcloud docker -- push gcr.io/$(GCLOUD_PROJECT)/bookshelf
25 |
26 | .PHONY: template
27 | template:
28 | sed -i ".tmpl" "s/\[GCLOUD_PROJECT\]/$(GCLOUD_PROJECT)/g" bookshelf-frontend.yaml
29 | sed -i ".tmpl" "s/\[GCLOUD_PROJECT\]/$(GCLOUD_PROJECT)/g" bookshelf-worker.yaml
30 |
31 | .PHONY: create-service
32 | create-service:
33 | kubectl create -f bookshelf-service.yaml
34 |
35 | .PHONY: deploy-frontend
36 | deploy-frontend: push template
37 | kubectl create -f bookshelf-frontend.yaml
38 |
39 | .PHONY: deploy-worker
40 | deploy-worker: push template
41 | kubectl create -f bookshelf-worker.yaml
42 |
43 | .PHONY: deploy
44 | deploy: deploy-frontend deploy-worker create-service
45 |
46 | .PHONY: delete
47 | delete:
48 | -kubectl delete -f bookshelf-service.yaml
49 | -kubectl delete -f bookshelf-worker.yaml
50 | -kubectl delete -f bookshelf-frontend.yaml
51 |
--------------------------------------------------------------------------------
/optional-kubernetes-engine/README.md:
--------------------------------------------------------------------------------
1 | # Deploy Bookshelf to Google Kubernetes Engine
2 |
3 | This optional tutorial will walk you through how to deploy the Bookshelf sample application to [Google Kubernetes Engine](https://cloud.google.com/kubernetes-engine/). This tutorial is also applicable to [Kubernetes](http://kubernetes.io/) outside of Google Kubernetes Engine, but may require additional steps for external load balancing.
4 |
5 | ## Pre-requisites
6 |
7 | 1. Create a project in the [Google Cloud Platform Console](https://console.cloud.google.com).
8 |
9 | 2. [Enable billing](https://console.cloud.google.com/project/_/settings) for your project.
10 |
11 | 3. [Enable APIs](https://console.cloud.google.com/flows/enableapi?apiid=datastore,pubsub,storage_api,logging,plus) for your project. The provided link will enable all necessary APIs, but if you wish to do so manually you will need Datastore, Pub/Sub, Storage, and Logging.
12 |
13 | 4. Install the [Google Cloud SDK](https://cloud.google.com/sdk)
14 |
15 | $ curl https://sdk.cloud.google.com | bash
16 | $ gcloud init
17 |
18 | 5. Install [Docker](https://www.docker.com/).
19 |
20 | ## Create a cluster
21 |
22 | Create a cluster for the bookshelf application:
23 |
24 | gcloud container clusters create bookshelf \
25 | --scopes "cloud-platform" \
26 | --num-nodes 2
27 | gcloud container clusters get-credentials bookshelf
28 |
29 | The scopes specified in the `--scopes` argument allows nodes in the cluster to access Google Cloud Platform APIs, such as the Cloud Datastore API.
30 |
31 | Alternatively, you can use make:
32 |
33 | make create-cluster
34 |
35 | ## Create a Cloud Storage bucket
36 |
37 | The bookshelf application uses [Google Cloud Storage](https://cloud.google.com/storage) to store image files. Create a bucket for your project:
38 |
39 | gsutil mb gs://
40 | gsutil defacl set public-read gs://
41 |
42 | Alternatively, you can use make:
43 |
44 | make create-bucket
45 |
46 | ## Update config.py
47 |
48 | Modify config.py and enter your Cloud Project ID into the `PROJECT_ID` and `CLOUD_STORAGE_BUCKET` field. The remaining configuration values are only needed if you wish to use a different database or if you wish to enable log-in via oauth2, which requires a domain name.
49 |
50 | ## Build the bookshelf container
51 |
52 | Before the application can be deployed to Kubernetes Engine, you will need build and push the image to [Google Container Registry](https://cloud.google.com/container-registry/).
53 |
54 | docker build -t gcr.io//bookshelf .
55 | gcloud docker push gcr.io//bookshelf
56 |
57 | Alternatively, you can use make:
58 |
59 | make push
60 |
61 | ## Deploy the bookshelf frontend
62 |
63 | The bookshelf app has two distinct "tiers". The frontend serves a web interface to create and manage books, while the worker handles fetching book information from the Google Books API.
64 |
65 | Update `bookshelf-frontend.yaml` with your Project ID or use `make template`. This file contains the Kubernetes resource definitions to deploy the frontend. You can use `kubectl` to create these resources on the cluster:
66 |
67 | kubectl create -f bookshelf-frontend.yaml
68 |
69 | Alternatively, you can use make:
70 |
71 | make deploy-frontend
72 |
73 | Once the resources are created, there should be 3 `bookshelf-frontend` pods on the cluster. To see the pods and ensure that they are running:
74 |
75 | kubectl get pods
76 |
77 | If the pods are not ready or if you see restarts, you can get the logs for a particular pod to figure out the issue:
78 |
79 | kubectl logs pod-id
80 |
81 | Once the pods are ready, you can get the public IP address of the load balancer:
82 |
83 | kubectl get services bookshelf-frontend
84 |
85 | You can then browse to the public IP address in your browser to see the bookshelf application.
86 |
87 | ## Deploy worker
88 |
89 | Update `bookshelf-worker.yaml` with your Project ID or use `make template`. This file contains the Kubernetes resource definitions to deploy the worker. The worker doesn't need to serve web traffic or expose any ports, so it has significantly less configuration than the frontend. You can use `kubectl` to create these resources on the cluster:
90 |
91 | kubectl create -f bookshelf-worker.yaml
92 |
93 | Alternatively, you can use make:
94 |
95 | make deploy-worker
96 |
97 | Once again, use `kubectl get pods` to check the status of the worker pods. Once the worker pods are up and running, you should be able to create books on the frontend and the workers will handle updating book information in the background.
98 |
--------------------------------------------------------------------------------
/optional-kubernetes-engine/bookshelf-frontend.yaml:
--------------------------------------------------------------------------------
1 | # Copyright 2015 Google Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License
14 |
15 | # This file configures the bookshelf application frontend. The frontend serves
16 | # public web traffic.
17 |
18 | apiVersion: apps/v1
19 | kind: Deployment
20 | metadata:
21 | name: bookshelf-frontend
22 | labels:
23 | app: bookshelf
24 | # The bookshelf frontend replica set ensures that at least 3
25 | # instances of the bookshelf app are running on the cluster.
26 | # For more info about Pods see:
27 | # https://cloud.google.com/kubernetes-engine/docs/pods/
28 | spec:
29 | replicas: 3
30 | selector:
31 | matchLabels:
32 | app: bookshelf
33 | tier: frontend
34 | template:
35 | metadata:
36 | labels:
37 | app: bookshelf
38 | tier: frontend
39 | spec:
40 | containers:
41 | - name: bookshelf-app
42 | # Replace [GCLOUD_PROJECT] with your project ID or use `make template`.
43 | image: gcr.io/[GCLOUD_PROJECT]/bookshelf
44 | # This setting makes nodes pull the docker image every time before
45 | # starting the pod. This is useful when debugging, but should be turned
46 | # off in production.
47 | imagePullPolicy: Always
48 | # The PROCESSES environment variable is used by Honcho in the
49 | # Dockerfile's CMD to control which processes are started. In this
50 | # case, only the bookshelf process is needed.
51 | env:
52 | - name: PROCESSES
53 | value: bookshelf
54 | # The bookshelf process listens on port 8080 for web traffic by default.
55 | ports:
56 | - name: http-server
57 | containerPort: 8080
58 |
--------------------------------------------------------------------------------
/optional-kubernetes-engine/bookshelf-service.yaml:
--------------------------------------------------------------------------------
1 | # Copyright 2016 Google Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License
14 |
15 | # The bookshelf service provides a load-balancing proxy over the bookshelf
16 | # frontend pods. By specifying the type as a 'LoadBalancer', Kubernetes Engine
17 | # will create an external HTTP load balancer.
18 | # For more information about Services see:
19 | # https://cloud.google.com/kubernetes-engine/docs/services/
20 | # For more information about external HTTP load balancing see:
21 | # https://cloud.google.com/kubernetes-engine/docs/load-balancer
22 | apiVersion: v1
23 | kind: Service
24 | metadata:
25 | name: bookshelf-frontend
26 | labels:
27 | app: bookshelf
28 | tier: frontend
29 | spec:
30 | type: LoadBalancer
31 | ports:
32 | - port: 80
33 | targetPort: http-server
34 | selector:
35 | app: bookshelf
36 | tier: frontend
37 |
--------------------------------------------------------------------------------
/optional-kubernetes-engine/bookshelf-worker.yaml:
--------------------------------------------------------------------------------
1 | # Copyright 2015 Google Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License
14 |
15 | # This file configures the bookshelf task worker. The worker is responsible
16 | # for processing book requests and updating book information.
17 |
18 | apiVersion: apps/v1
19 | kind: Deployment
20 | metadata:
21 | name: bookshelf-worker
22 | labels:
23 | app: bookshelf
24 | # The bookshelf worker replica set ensures that at least 2 instances of the
25 | # bookshelf worker pod are running on the cluster.
26 | # For more info about Pods see:
27 | # https://cloud.google.com/kubernetes-engine/docs/pods/
28 | spec:
29 | replicas: 2
30 | selector:
31 | matchLabels:
32 | app: bookshelf
33 | tier: worker
34 | template:
35 | metadata:
36 | labels:
37 | app: bookshelf
38 | tier: worker
39 | spec:
40 | containers:
41 | - name: bookshelf-app
42 | # Replace [GCLOUD_PROJECT] with your project ID or use `make template`.
43 | image: gcr.io/[GCLOUD_PROJECT]/bookshelf
44 | # This setting makes nodes pull the docker image every time before
45 | # starting the pod. This is useful when debugging, but should be turned
46 | # off in production.
47 | imagePullPolicy: Always
48 | # The PROCESSES environment variable is used by Honcho in the
49 | # Dockerfile's CMD to control which processes are started. In this
50 | # case, only the worker process is needed.
51 | env:
52 | - name: PROCESSES
53 | value: worker
54 |
--------------------------------------------------------------------------------
/optional-kubernetes-engine/bookshelf/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright 2015 Google Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import json
16 | import logging
17 |
18 | from flask import current_app, Flask, redirect, request, session, url_for
19 | import httplib2
20 | from oauth2client.contrib.flask_util import UserOAuth2
21 |
22 |
23 | oauth2 = UserOAuth2()
24 |
25 |
26 | def create_app(config, debug=False, testing=False, config_overrides=None):
27 | app = Flask(__name__)
28 | app.config.from_object(config)
29 |
30 | app.debug = debug
31 | app.testing = testing
32 |
33 | if config_overrides:
34 | app.config.update(config_overrides)
35 |
36 | # Configure logging
37 | if not app.testing:
38 | logging.basicConfig(level=logging.INFO)
39 |
40 | # Setup the data model.
41 | with app.app_context():
42 | model = get_model()
43 | model.init_app(app)
44 |
45 | # Create a health check handler. Health checks are used when running on
46 | # Google Compute Engine by the load balancer to determine which instances
47 | # can serve traffic. Google App Engine also uses health checking, but
48 | # accepts any non-500 response as healthy.
49 | @app.route('/_ah/health')
50 | def health_check():
51 | return 'ok', 200
52 |
53 | # Initalize the OAuth2 helper.
54 | oauth2.init_app(
55 | app,
56 | scopes=['email', 'profile'],
57 | authorize_callback=_request_user_info)
58 |
59 | # Add a logout handler.
60 | @app.route('/logout')
61 | def logout():
62 | # Delete the user's profile and the credentials stored by oauth2.
63 | del session['profile']
64 | session.modified = True
65 | oauth2.storage.delete()
66 | return redirect(request.referrer or '/')
67 |
68 | # Register the Bookshelf CRUD blueprint.
69 | from .crud import crud
70 | app.register_blueprint(crud, url_prefix='/books')
71 |
72 | # Add a default root route.
73 | @app.route("/")
74 | def index():
75 | return redirect(url_for('crud.list'))
76 |
77 | # Add an error handler. This is useful for debugging the live application,
78 | # however, you should disable the output of the exception for production
79 | # applications.
80 | @app.errorhandler(500)
81 | def server_error(e):
82 | return """
83 | An internal error occurred:
{}
84 | See logs for full stacktrace.
85 | """.format(e), 500
86 |
87 | return app
88 |
89 |
90 | def get_model():
91 | model_backend = current_app.config['DATA_BACKEND']
92 | if model_backend == 'cloudsql':
93 | from . import model_cloudsql
94 | model = model_cloudsql
95 | elif model_backend == 'datastore':
96 | from . import model_datastore
97 | model = model_datastore
98 | elif model_backend == 'mongodb':
99 | from . import model_mongodb
100 | model = model_mongodb
101 | else:
102 | raise ValueError(
103 | "No appropriate databackend configured. "
104 | "Please specify datastore, cloudsql, or mongodb")
105 |
106 | return model
107 |
108 |
109 | def _request_user_info(credentials):
110 | """
111 | Makes an HTTP request to the Google OAuth2 API to retrieve the user's basic
112 | profile information, including full name and photo, and stores it in the
113 | Flask session.
114 | """
115 | http = httplib2.Http()
116 | credentials.authorize(http)
117 | resp, content = http.request(
118 | 'https://www.googleapis.com/oauth2/v3/userinfo')
119 |
120 | if resp.status != 200:
121 | current_app.logger.error(
122 | "Error while obtaining user profile: \n%s: %s", resp, content)
123 | return None
124 |
125 | session['profile'] = json.loads(content.decode('utf-8'))
126 |
--------------------------------------------------------------------------------
/optional-kubernetes-engine/bookshelf/crud.py:
--------------------------------------------------------------------------------
1 | # Copyright 2015 Google Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | from bookshelf import get_model, oauth2, storage, tasks
16 | from flask import Blueprint, current_app, redirect, render_template, request, \
17 | session, url_for
18 |
19 |
20 | crud = Blueprint('crud', __name__)
21 |
22 |
23 | def upload_image_file(file):
24 | """
25 | Upload the user-uploaded file to Google Cloud Storage and retrieve its
26 | publicly-accessible URL.
27 | """
28 | if not file:
29 | return None
30 |
31 | public_url = storage.upload_file(
32 | file.read(),
33 | file.filename,
34 | file.content_type
35 | )
36 |
37 | current_app.logger.info(
38 | "Uploaded file %s as %s.", file.filename, public_url)
39 |
40 | return public_url
41 |
42 |
43 | @crud.route("/")
44 | def list():
45 | token = request.args.get('page_token', None)
46 | if token:
47 | token = token.encode('utf-8')
48 |
49 | books, next_page_token = get_model().list(cursor=token)
50 |
51 | return render_template(
52 | "list.html",
53 | books=books,
54 | next_page_token=next_page_token)
55 |
56 |
57 | @crud.route("/mine")
58 | @oauth2.required
59 | def list_mine():
60 | token = request.args.get('page_token', None)
61 | if token:
62 | token = token.encode('utf-8')
63 |
64 | books, next_page_token = get_model().list_by_user(
65 | user_id=session['profile']['id'],
66 | cursor=token)
67 |
68 | return render_template(
69 | "list.html",
70 | books=books,
71 | next_page_token=next_page_token)
72 |
73 |
74 | @crud.route('/')
75 | def view(id):
76 | book = get_model().read(id)
77 | return render_template("view.html", book=book)
78 |
79 |
80 | @crud.route('/add', methods=['GET', 'POST'])
81 | def add():
82 | if request.method == 'POST':
83 | data = request.form.to_dict(flat=True)
84 |
85 | # If an image was uploaded, update the data to point to the new image.
86 | image_url = upload_image_file(request.files.get('image'))
87 |
88 | if image_url:
89 | data['imageUrl'] = image_url
90 |
91 | # If the user is logged in, associate their profile with the new book.
92 | if 'profile' in session:
93 | data['createdBy'] = session['profile']['name']
94 | data['createdById'] = session['profile']['email']
95 |
96 | book = get_model().create(data)
97 |
98 | q = tasks.get_books_queue()
99 | q.enqueue(tasks.process_book, book['id'])
100 |
101 | return redirect(url_for('.view', id=book['id']))
102 |
103 | return render_template("form.html", action="Add", book={})
104 |
105 |
106 | @crud.route('//edit', methods=['GET', 'POST'])
107 | def edit(id):
108 | book = get_model().read(id)
109 |
110 | if request.method == 'POST':
111 | data = request.form.to_dict(flat=True)
112 |
113 | image_url = upload_image_file(request.files.get('image'))
114 |
115 | if image_url:
116 | data['imageUrl'] = image_url
117 |
118 | book = get_model().update(data, id)
119 |
120 | q = tasks.get_books_queue()
121 | q.enqueue(tasks.process_book, book['id'])
122 |
123 | return redirect(url_for('.view', id=book['id']))
124 |
125 | return render_template("form.html", action="Edit", book=book)
126 |
127 |
128 | @crud.route('//delete')
129 | def delete(id):
130 | get_model().delete(id)
131 | return redirect(url_for('.list'))
132 |
--------------------------------------------------------------------------------
/optional-kubernetes-engine/bookshelf/model_cloudsql.py:
--------------------------------------------------------------------------------
1 | # Copyright 2015 Google Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | from flask import Flask
16 | from flask_sqlalchemy import SQLAlchemy
17 |
18 |
19 | builtin_list = list
20 |
21 |
22 | db = SQLAlchemy()
23 |
24 |
25 | def init_app(app):
26 | # Disable track modifications, as it unnecessarily uses memory.
27 | app.config.setdefault('SQLALCHEMY_TRACK_MODIFICATIONS', False)
28 | db.init_app(app)
29 |
30 |
31 | def from_sql(row):
32 | """Translates a SQLAlchemy model instance into a dictionary"""
33 | data = row.__dict__.copy()
34 | data['id'] = row.id
35 | data.pop('_sa_instance_state')
36 | return data
37 |
38 |
39 | class Book(db.Model):
40 | __tablename__ = 'books'
41 |
42 | id = db.Column(db.Integer, primary_key=True)
43 | title = db.Column(db.String(255))
44 | author = db.Column(db.String(255))
45 | publishedDate = db.Column(db.String(255))
46 | imageUrl = db.Column(db.String(255))
47 | description = db.Column(db.String(4096))
48 | createdBy = db.Column(db.String(255))
49 | createdById = db.Column(db.String(255))
50 |
51 | def __repr__(self):
52 | return "
17 |
18 |
19 | Bookshelf - Python on Google Cloud Platform
20 |
21 |
22 |
23 |
24 |
25 |
38 | {{user}}
39 |
40 |
41 |
--------------------------------------------------------------------------------
/optional-kubernetes-engine/bookshelf/templates/form.html:
--------------------------------------------------------------------------------
1 | {#
2 | # Copyright 2015 Google Inc.
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 | #}
16 |
17 | {% extends "base.html" %}
18 |
19 | {% block content %}
20 |
{{action}} book
21 |
22 |
66 |
67 | {% endblock %}
68 |
--------------------------------------------------------------------------------
/optional-kubernetes-engine/bookshelf/templates/list.html:
--------------------------------------------------------------------------------
1 | {#
2 | # Copyright 2015 Google Inc.
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 | #}
16 |
17 | {% extends "base.html" %}
18 |
19 | {% block content %}
20 |
21 |
45 | {% endfor %}
46 |
47 | {% if next_page_token %}
48 |
53 | {% endif %}
54 |
55 | {% endblock %}
56 |
--------------------------------------------------------------------------------
/optional-kubernetes-engine/bookshelf/templates/view.html:
--------------------------------------------------------------------------------
1 | {#
2 | # Copyright 2015 Google Inc.
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 | #}
16 |
17 | {% extends "base.html" %}
18 |
19 | {% block content %}
20 |
21 |
49 | Added by {{book.createdBy|default('Anonymous', True)}}
50 |
51 |
52 |
53 | {% endblock %}
54 |
--------------------------------------------------------------------------------
/optional-kubernetes-engine/config.py:
--------------------------------------------------------------------------------
1 | # Copyright 2015 Google Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """
16 | This file contains all of the configuration values for the application.
17 | Update this file with the values for your specific Google Cloud project.
18 | You can create and manage projects at https://console.developers.google.com
19 | """
20 |
21 | import os
22 |
23 | # The secret key is used by Flask to encrypt session cookies.
24 | SECRET_KEY = 'secret'
25 |
26 | # There are three different ways to store the data in the application.
27 | # You can choose 'datastore', 'cloudsql', or 'mongodb'. Be sure to
28 | # configure the respective settings for the one you choose below.
29 | # You do not have to configure the other data backends. If unsure, choose
30 | # 'datastore' as it does not require any additional configuration.
31 | DATA_BACKEND = 'datastore'
32 |
33 | # Google Cloud Project ID. This can be found on the 'Overview' page at
34 | # https://console.developers.google.com
35 | PROJECT_ID = 'your-project-id'
36 |
37 | # CloudSQL & SQLAlchemy configuration
38 | # Replace the following values the respective values of your Cloud SQL
39 | # instance.
40 | CLOUDSQL_USER = 'root'
41 | CLOUDSQL_PASSWORD = 'your-cloudsql-password'
42 | CLOUDSQL_DATABASE = 'bookshelf'
43 | # Set this value to the Cloud SQL connection name, e.g.
44 | # "project:region:cloudsql-instance".
45 | # You must also update the value in app.yaml.
46 | CLOUDSQL_CONNECTION_NAME = 'your-cloudsql-connection-name'
47 |
48 | # The CloudSQL proxy is used locally to connect to the cloudsql instance.
49 | # To start the proxy, use:
50 | #
51 | # $ cloud_sql_proxy -instances=your-connection-name=tcp:3306
52 | #
53 | # Alternatively, you could use a local MySQL instance for testing.
54 | LOCAL_SQLALCHEMY_DATABASE_URI = (
55 | 'mysql+pymysql://{user}:{password}@localhost/{database}').format(
56 | user=CLOUDSQL_USER, password=CLOUDSQL_PASSWORD,
57 | database=CLOUDSQL_DATABASE)
58 |
59 | # When running on App Engine a unix socket is used to connect to the cloudsql
60 | # instance.
61 | LIVE_SQLALCHEMY_DATABASE_URI = (
62 | 'mysql+pymysql://{user}:{password}@localhost/{database}'
63 | '?unix_socket=/cloudsql/{connection_name}').format(
64 | user=CLOUDSQL_USER, password=CLOUDSQL_PASSWORD,
65 | database=CLOUDSQL_DATABASE, connection_name=CLOUDSQL_CONNECTION_NAME)
66 |
67 | if os.environ.get('GAE_INSTANCE'):
68 | SQLALCHEMY_DATABASE_URI = LIVE_SQLALCHEMY_DATABASE_URI
69 | else:
70 | SQLALCHEMY_DATABASE_URI = LOCAL_SQLALCHEMY_DATABASE_URI
71 |
72 | # Mongo configuration
73 | # If using mongolab, the connection URI is available from the mongolab control
74 | # panel. If self-hosting on compute engine, replace the values below.
75 | MONGO_URI = 'mongodb://user:password@host:27017/database'
76 |
77 | # Google Cloud Storage and upload settings.
78 | # Typically, you'll name your bucket the same as your project. To create a
79 | # bucket:
80 | #
81 | # $ gsutil mb gs://
82 | #
83 | # You also need to make sure that the default ACL is set to public-read,
84 | # otherwise users will not be able to see their upload images:
85 | #
86 | # $ gsutil defacl set public-read gs://
87 | #
88 | # You can adjust the max content length and allow extensions settings to allow
89 | # larger or more varied file types if desired.
90 | CLOUD_STORAGE_BUCKET = 'your-bucket-name'
91 | MAX_CONTENT_LENGTH = 8 * 1024 * 1024
92 | ALLOWED_EXTENSIONS = set(['png', 'jpg', 'jpeg', 'gif'])
93 |
94 | # OAuth2 configuration.
95 | # This can be generated from the Google Developers Console at
96 | # https://console.developers.google.com/project/_/apiui/credential.
97 | # Note that you will need to add all URLs that your application uses as
98 | # authorized redirect URIs. For example, typically you would add the following:
99 | #
100 | # * http://localhost:8080/oauth2callback
101 | # * https://.appspot.com/oauth2callback.
102 | #
103 | # If you receive a invalid redirect URI error review you settings to ensure
104 | # that the current URI is allowed.
105 | GOOGLE_OAUTH2_CLIENT_ID = \
106 | 'your-client-id'
107 | GOOGLE_OAUTH2_CLIENT_SECRET = 'your-client-secret'
108 |
--------------------------------------------------------------------------------
/optional-kubernetes-engine/main.py:
--------------------------------------------------------------------------------
1 | # Copyright 2015 Google Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import bookshelf
16 | import config
17 |
18 |
19 | # Note: debug=True is enabled here to help with troubleshooting. You should
20 | # remove this in production.
21 | app = bookshelf.create_app(config, debug=True)
22 |
23 |
24 | # Make the queue available at the top-level, this allows you to run
25 | # `psqworker main.books_queue`. We have to use the app's context because
26 | # it contains all the configuration for plugins.
27 | # If you were using another task queue, such as celery or rq, you can use this
28 | # section to configure your queues to work with Flask.
29 | with app.app_context():
30 | books_queue = bookshelf.tasks.get_books_queue()
31 |
32 |
33 | # This is only used when running locally. When running live, gunicorn runs
34 | # the application.
35 | if __name__ == '__main__':
36 | app.run(host='127.0.0.1', port=8080, debug=True)
37 |
--------------------------------------------------------------------------------
/optional-kubernetes-engine/procfile:
--------------------------------------------------------------------------------
1 | bookshelf: gunicorn -b 0.0.0.0:$PORT main:app
2 | worker: psqworker main.books_queue
3 |
--------------------------------------------------------------------------------
/optional-kubernetes-engine/requirements-dev.txt:
--------------------------------------------------------------------------------
1 | tox==3.5.3
2 | flake8==3.6.0
3 | flaky==3.4.0
4 | pytest==4.0.1
5 | pytest-cov==2.6.0
6 | BeautifulSoup4==4.6.3
7 | requests==2.20.1
8 | retrying==1.3.3
9 |
--------------------------------------------------------------------------------
/optional-kubernetes-engine/requirements.txt:
--------------------------------------------------------------------------------
1 | Flask==1.0.4
2 | google-cloud-datastore==1.7.1
3 | google-cloud-storage==1.23.0
4 | google-cloud-logging==1.8.0
5 | google-cloud-error_reporting==0.30.0
6 | gunicorn==19.9.0
7 | oauth2client==4.1.3
8 | mock==2.0.0
9 | Flask-SQLAlchemy==2.3.2
10 | PyMySQL==0.9.2
11 | Flask-PyMongo==2.3.0
12 | PyMongo==3.7.2
13 | six==1.11.0
14 | requests[security]==2.21.0
15 | honcho==1.0.1
16 | psq==0.7.0
17 |
--------------------------------------------------------------------------------
/optional-kubernetes-engine/tests/conftest.py:
--------------------------------------------------------------------------------
1 | # Copyright 2015 Google Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """conftest.py is used to define common test fixtures for pytest."""
16 |
17 | import bookshelf
18 | import config
19 | from google.cloud.exceptions import ServiceUnavailable
20 | from oauth2client.client import HttpAccessTokenRefreshError
21 | import pytest
22 | from retrying import retry
23 |
24 |
25 | @pytest.fixture(params=['datastore', 'mongodb'])
26 | def app(request):
27 | """This fixtures provides a Flask app instance configured for testing.
28 |
29 | Because it's parametric, it will cause every test that uses this fixture
30 | to run three times: one time for each backend (datastore, cloudsql, and
31 | mongodb).
32 |
33 | It also ensures the tests run within a request context, allowing
34 | any calls to flask.request, flask.current_app, etc. to work."""
35 | app = bookshelf.create_app(
36 | config,
37 | testing=True,
38 | config_overrides={
39 | 'DATA_BACKEND': request.param
40 | })
41 |
42 | with app.test_request_context():
43 | yield app
44 |
45 |
46 | @pytest.fixture
47 | def model(monkeypatch, app):
48 | """This fixture provides a modified version of the app's model that tracks
49 | all created items and deletes them at the end of the test.
50 |
51 | Any tests that directly or indirectly interact with the database should use
52 | this to ensure that resources are properly cleaned up.
53 |
54 | Monkeypatch is provided by pytest and used to patch the model's create
55 | method.
56 |
57 | The app fixture is needed to provide the configuration and context needed
58 | to get the proper model object.
59 | """
60 | model = bookshelf.get_model()
61 |
62 | # Ensure no books exist before running. This typically helps if tests
63 | # somehow left the database in a bad state.
64 | delete_all_books(model)
65 |
66 | yield model
67 |
68 | # Delete all books that we created during tests.
69 | delete_all_books(model)
70 |
71 |
72 | # The backend data stores can sometimes be flaky. It's useful to retry this
73 | # a few times before giving up.
74 | @retry(
75 | stop_max_attempt_number=3,
76 | wait_exponential_multiplier=100,
77 | wait_exponential_max=2000)
78 | def delete_all_books(model):
79 | while True:
80 | books, _ = model.list(limit=50)
81 | if not books:
82 | break
83 | for book in books:
84 | model.delete(book['id'])
85 |
86 |
87 | def flaky_filter(info, *args):
88 | """Used by flaky to determine when to re-run a test case."""
89 | _, e, _ = info
90 | return isinstance(e, (ServiceUnavailable, HttpAccessTokenRefreshError))
91 |
--------------------------------------------------------------------------------
/optional-kubernetes-engine/tests/test_auth.py:
--------------------------------------------------------------------------------
1 | # Copyright 2015 Google Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import contextlib
16 |
17 | import bookshelf
18 | from conftest import flaky_filter
19 | from flaky import flaky
20 | import mock
21 | from oauth2client.client import OAuth2Credentials
22 | import pytest
23 |
24 |
25 | @pytest.fixture
26 | def client_with_credentials(app):
27 | """This fixture provides a Flask app test client that has a session
28 | pre-configured with use credentials."""
29 | credentials = OAuth2Credentials(
30 | 'access_token',
31 | 'client_id',
32 | 'client_secret',
33 | 'refresh_token',
34 | '3600',
35 | None,
36 | 'Test',
37 | id_token={'sub': '123', 'email': 'user@example.com'},
38 | scopes=('email', 'profile'))
39 |
40 | @contextlib.contextmanager
41 | def inner():
42 | with app.test_client() as client:
43 | with client.session_transaction() as session:
44 | session['profile'] = {
45 | 'email': 'abc@example.com',
46 | 'name': 'Test User'
47 | }
48 | session['google_oauth2_credentials'] = credentials.to_json()
49 | yield client
50 |
51 | return inner
52 |
53 |
54 | # Mark all test cases in this class as flaky, so that if errors occur they
55 | # can be retried. This is useful when databases are temporarily unavailable.
56 | @flaky(rerun_filter=flaky_filter)
57 | # Tell pytest to use both the app and model fixtures for all test cases.
58 | # This ensures that configuration is properly applied and that all database
59 | # resources created during tests are cleaned up. These fixtures are defined
60 | # in conftest.py
61 | @pytest.mark.usefixtures('app', 'model')
62 | class TestAuth(object):
63 | def test_not_logged_in(self, app):
64 | with app.test_client() as c:
65 | rv = c.get('/books/')
66 |
67 | assert rv.status == '200 OK'
68 | body = rv.data.decode('utf-8')
69 | assert 'Login' in body
70 |
71 | def test_logged_in(self, client_with_credentials):
72 | with client_with_credentials() as c:
73 | rv = c.get('/books/')
74 |
75 | assert rv.status == '200 OK'
76 | body = rv.data.decode('utf-8')
77 | assert 'Test User' in body
78 |
79 | def test_add_anonymous(self, app):
80 | data = {
81 | 'title': 'Test Book',
82 | }
83 |
84 | with app.test_client() as c:
85 | rv = c.post('/books/add', data=data, follow_redirects=True)
86 |
87 | assert rv.status == '200 OK'
88 | body = rv.data.decode('utf-8')
89 | assert 'Test Book' in body
90 | assert 'Added by Anonymous' in body
91 |
92 | def test_add_logged_in(self, client_with_credentials):
93 | data = {
94 | 'title': 'Test Book',
95 | }
96 |
97 | with client_with_credentials() as c:
98 | rv = c.post('/books/add', data=data, follow_redirects=True)
99 |
100 | assert rv.status == '200 OK'
101 | body = rv.data.decode('utf-8')
102 | assert 'Test Book' in body
103 | assert 'Added by Test User' in body
104 |
105 | def test_mine(self, model, client_with_credentials):
106 | # Create two books, one created by the logged in user and one
107 | # created by another user.
108 | model.create({
109 | 'title': 'Book 1',
110 | 'createdById': 'abc@example.com'
111 | })
112 |
113 | model.create({
114 | 'title': 'Book 2',
115 | 'createdById': 'def@example.com'
116 | })
117 |
118 | # Check the "My Books" page and make sure only one of the books
119 | # appears.
120 | with client_with_credentials() as c:
121 | rv = c.get('/books/mine')
122 |
123 | assert rv.status == '200 OK'
124 | body = rv.data.decode('utf-8')
125 | assert 'Book 1' in body
126 | assert 'Book 2' not in body
127 |
128 | @mock.patch("httplib2.Http")
129 | def test_request_user_info(self, HttpMock):
130 | httpObj = mock.MagicMock()
131 | responseMock = mock.MagicMock(status=200)
132 | httpObj.request = mock.MagicMock(
133 | return_value=(responseMock, b'{"name": "bill"}'))
134 | HttpMock.return_value = httpObj
135 | credentials = mock.MagicMock()
136 | bookshelf._request_user_info(credentials)
137 |
--------------------------------------------------------------------------------
/optional-kubernetes-engine/tests/test_crud.py:
--------------------------------------------------------------------------------
1 | # Copyright 2015 Google Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import re
16 |
17 | from conftest import flaky_filter
18 | from flaky import flaky
19 | import pytest
20 |
21 |
22 | # Mark all test cases in this class as flaky, so that if errors occur they
23 | # can be retried. This is useful when databases are temporarily unavailable.
24 | @flaky(rerun_filter=flaky_filter)
25 | # Tell pytest to use both the app and model fixtures for all test cases.
26 | # This ensures that configuration is properly applied and that all database
27 | # resources created during tests are cleaned up. These fixtures are defined
28 | # in conftest.py
29 | @pytest.mark.usefixtures('app', 'model')
30 | class TestCrudActions(object):
31 |
32 | def test_list(self, app, model):
33 | for i in range(1, 12):
34 | model.create({'title': u'Book {0}'.format(i)})
35 |
36 | with app.test_client() as c:
37 | rv = c.get('/books/')
38 |
39 | assert rv.status == '200 OK'
40 |
41 | body = rv.data.decode('utf-8')
42 | assert 'Book 1' in body, "Should show books"
43 | assert len(re.findall('
Book', body)) == 10, (
44 | "Should not show more than 10 books")
45 | assert 'More' in body, "Should have more than one page"
46 |
47 | def test_add(self, app):
48 | data = {
49 | 'title': 'Test Book',
50 | 'author': 'Test Author',
51 | 'publishedDate': 'Test Date Published',
52 | 'description': 'Test Description'
53 | }
54 |
55 | with app.test_client() as c:
56 | rv = c.post('/books/add', data=data, follow_redirects=True)
57 |
58 | assert rv.status == '200 OK'
59 | body = rv.data.decode('utf-8')
60 | assert 'Test Book' in body
61 | assert 'Test Author' in body
62 | assert 'Test Date Published' in body
63 | assert 'Test Description' in body
64 |
65 | def test_edit(self, app, model):
66 | existing = model.create({'title': "Temp Title"})
67 |
68 | with app.test_client() as c:
69 | rv = c.post(
70 | '/books/%s/edit' % existing['id'],
71 | data={'title': 'Updated Title'},
72 | follow_redirects=True)
73 |
74 | assert rv.status == '200 OK'
75 | body = rv.data.decode('utf-8')
76 | assert 'Updated Title' in body
77 | assert 'Temp Title' not in body
78 |
79 | def test_delete(self, app, model):
80 | existing = model.create({'title': "Temp Title"})
81 |
82 | with app.test_client() as c:
83 | rv = c.get(
84 | '/books/%s/delete' % existing['id'],
85 | follow_redirects=True)
86 |
87 | assert rv.status == '200 OK'
88 | assert not model.read(existing['id'])
89 |
--------------------------------------------------------------------------------
/optional-kubernetes-engine/tests/test_end_to_end.py:
--------------------------------------------------------------------------------
1 | # Copyright 2015 Google Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import os
16 | import re
17 |
18 | from bs4 import BeautifulSoup
19 | import pytest
20 | import requests
21 | from retrying import retry
22 |
23 |
24 | @pytest.mark.e2e
25 | def test_end_to_end():
26 | """Tests designed to be run against live environments.
27 |
28 | Unlike the integration tests in the other packages, these tests are
29 | designed to be run against fully-functional live environments.
30 |
31 | To run locally, start both main.py and psq_worker main.books_queue and
32 | run this file.
33 |
34 | It can be run against a live environment by setting the E2E_URL
35 | environment variables before running the tests:
36 |
37 | E2E_URL=http://your-app-id.appspot.com \
38 | nosetests tests/test_end_to_end.py
39 | """
40 |
41 | base_url = os.environ.get('E2E_URL', 'http://localhost:8080')
42 |
43 | book_data = {
44 | 'title': 'a confederacy of dunces',
45 | }
46 |
47 | response = requests.post(base_url + '/books/add', data=book_data)
48 |
49 | # There was a 302, so get the book's URL from the redirect.
50 | book_url = response.request.url
51 | book_id = book_url.rsplit('/', 1).pop()
52 |
53 | # Use retry because it will take some indeterminate time for the pub/sub
54 | # message to be processed.
55 | @retry(wait_exponential_multiplier=5000, stop_max_attempt_number=12)
56 | def check_for_updated_data():
57 | # Check that the book's information was updated.
58 | response = requests.get(book_url)
59 | assert response.status_code == 200
60 |
61 | soup = BeautifulSoup(response.text, 'html.parser')
62 |
63 | title = soup.find('h4', 'book-title').contents[0].strip()
64 | assert re.search(r'A Confederacy of Dunces', title, re.I)
65 |
66 | author = soup.find('h5', 'book-author').string
67 | assert re.search(r'John Kennedy Toole', author, re.I)
68 |
69 | description = soup.find('p', 'book-description').string
70 | assert re.search(r'Ignatius', description, re.I)
71 |
72 | image_src = soup.find('img', 'book-image')['src']
73 | image = requests.get(image_src)
74 | assert image.status_code == 200
75 |
76 | try:
77 | check_for_updated_data()
78 | finally:
79 | # Delete the book we created.
80 | requests.get(base_url + '/books/{}/delete'.format(book_id))
81 |
--------------------------------------------------------------------------------
/optional-kubernetes-engine/tests/test_storage.py:
--------------------------------------------------------------------------------
1 | # Copyright 2015 Google Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import re
16 |
17 | from conftest import flaky_filter
18 | from flaky import flaky
19 | import httplib2
20 | import pytest
21 | from six import BytesIO
22 |
23 |
24 | # Mark all test cases in this class as flaky, so that if errors occur they
25 | # can be retried. This is useful when databases are temporarily unavailable.
26 | @flaky(rerun_filter=flaky_filter)
27 | # Tell pytest to use both the app and model fixtures for all test cases.
28 | # This ensures that configuration is properly applied and that all database
29 | # resources created during tests are cleaned up. These fixtures are defined
30 | # in conftest.py
31 | @pytest.mark.usefixtures('app', 'model')
32 | class TestStorage(object):
33 |
34 | def test_upload_image(self, app):
35 | data = {
36 | 'title': 'Test Book',
37 | 'author': 'Test Author',
38 | 'publishedDate': 'Test Date Published',
39 | 'description': 'Test Description',
40 | 'image': (BytesIO(b'hello world'), 'hello.jpg')
41 | }
42 |
43 | with app.test_client() as c:
44 | rv = c.post('/books/add', data=data, follow_redirects=True)
45 |
46 | assert rv.status == '200 OK'
47 | body = rv.data.decode('utf-8')
48 |
49 | img_tag = re.search(''),
63 | '1337h4x0r.php')
64 | }
65 |
66 | with app.test_client() as c:
67 | rv = c.post('/books/add', data=data, follow_redirects=True)
68 |
69 | # check we weren't pwned
70 | assert rv.status == '400 BAD REQUEST'
71 |
--------------------------------------------------------------------------------
/optional-kubernetes-engine/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | skipsdist = True
3 | envlist = lint,py27,py36
4 |
5 | [testenv]
6 | deps =
7 | -rrequirements.txt
8 | -rrequirements-dev.txt
9 | commands =
10 | py.test --cov=bookshelf --no-success-flaky-report -m "not e2e" {posargs: tests}
11 | passenv = GOOGLE_APPLICATION_CREDENTIALS DATASTORE_HOST E2E_URL
12 | setenv = PYTHONPATH={toxinidir}
13 |
14 |
15 | [testenv:py27-e2e]
16 | basepython = python2.7
17 | commands =
18 | py.test --no-success-flaky-report -m "e2e" {posargs: tests}
19 |
20 | [testenv:py36-e2e]
21 | basepython = python3.6
22 | commands =
23 | py.test --no-success-flaky-report -m "e2e" {posargs: tests}
24 |
25 | [testenv:lint]
26 | deps =
27 | flake8
28 | flake8-import-order
29 | commands =
30 | flake8 --import-order-style=google bookshelf tests
31 |
--------------------------------------------------------------------------------
/pytest.ini:
--------------------------------------------------------------------------------
1 | [pytest]
2 | addopts =
3 | -v
4 | --tb=native
5 | norecursedirs = .git env lib .tox .nox
6 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | flake8===5.0.4; python_version < '3.8'
2 | flake8==6.0.0; python_version >= '3.8'
3 | pytest==7.3.1
4 | nox==2023.4.22
5 | requests==2.31.0
6 |
--------------------------------------------------------------------------------
/secrets.tar.enc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleCloudPlatform/getting-started-python/d9da8db203f7729f5da28b57be66b69084955bde/secrets.tar.enc
--------------------------------------------------------------------------------
/sessions/app.yaml:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Google LLC All Rights Reserved.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | # [START getting_started_sessions_runtime]
16 | runtime: python37
17 | # [END getting_started_sessions_runtime]
18 |
--------------------------------------------------------------------------------
/sessions/main.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Google LLC All Rights Reserved.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | # [START getting_started_sessions_all]
16 | import random
17 | from uuid import uuid4
18 |
19 | from flask import Flask, make_response, request
20 | from google.cloud import firestore
21 |
22 |
23 | app = Flask(__name__)
24 | db = firestore.Client()
25 | sessions = db.collection('sessions')
26 | greetings = [
27 | 'Hello World',
28 | 'Hallo Welt',
29 | 'Ciao Mondo',
30 | 'Salut le Monde',
31 | 'Hola Mundo',
32 | ]
33 |
34 |
35 | @firestore.transactional
36 | def get_session_data(transaction, session_id):
37 | """ Looks up (or creates) the session with the given session_id.
38 | Creates a random session_id if none is provided. Increments
39 | the number of views in this session. Updates are done in a
40 | transaction to make sure no saved increments are overwritten.
41 | """
42 | if session_id is None:
43 | session_id = str(uuid4()) # Random, unique identifier
44 |
45 | doc_ref = sessions.document(document_id=session_id)
46 | doc = doc_ref.get(transaction=transaction)
47 | if doc.exists:
48 | session = doc.to_dict()
49 | else:
50 | session = {
51 | 'greeting': random.choice(greetings),
52 | 'views': 0
53 | }
54 |
55 | session['views'] += 1 # This counts as a view
56 | transaction.set(doc_ref, session)
57 |
58 | session['session_id'] = session_id
59 | return session
60 |
61 |
62 | @app.route('/', methods=['GET'])
63 | def home():
64 | template = '{} views for "{}"'
65 |
66 | transaction = db.transaction()
67 | session = get_session_data(transaction, request.cookies.get('session_id'))
68 |
69 | resp = make_response(template.format(
70 | session['views'],
71 | session['greeting']
72 | )
73 | )
74 | resp.set_cookie('session_id', session['session_id'], httponly=True)
75 | return resp
76 |
77 |
78 | if __name__ == '__main__':
79 | app.run(host='127.0.0.1', port=8080)
80 | # [END getting_started_sessions_all]
81 |
--------------------------------------------------------------------------------
/sessions/main_test.py:
--------------------------------------------------------------------------------
1 | # Copyright 2019 Google LLC All Rights Reserved.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import re
16 | import uuid
17 |
18 | import main
19 | import pytest
20 |
21 |
22 | @pytest.fixture
23 | def client():
24 | """ Yields a test client, AND creates and later cleans up a
25 | dummy collection for sessions.
26 | """
27 | main.app.testing = True
28 |
29 | # Override the Firestore collection used for sessions in main
30 | main.sessions = main.db.collection(str(uuid.uuid4()))
31 |
32 | client = main.app.test_client()
33 | yield client
34 |
35 | # Clean up session objects created in test collection
36 | for doc_ref in main.sessions.list_documents():
37 | doc_ref.delete()
38 |
39 |
40 | def test_session(client):
41 | r = client.get('/')
42 | assert r.status_code == 200
43 | data = r.data.decode('utf-8')
44 | assert '1 views' in data
45 |
46 | match = re.search('views for "([A-Za-z ]+)"', data)
47 | assert match is not None
48 | greeting = match.group(1)
49 |
50 | r = client.get('/')
51 | assert r.status_code == 200
52 | data = r.data.decode('utf-8')
53 | assert '2 views' in data
54 | assert greeting in data
55 |
--------------------------------------------------------------------------------
/sessions/requirements-dev.txt:
--------------------------------------------------------------------------------
1 | pytest>=5.0.0
2 |
--------------------------------------------------------------------------------
/sessions/requirements.txt:
--------------------------------------------------------------------------------
1 | google-cloud-firestore==2.11.1
2 | flask==2.2.5
3 |
--------------------------------------------------------------------------------