diff --git a/scheduler/README.md b/scheduler/README.md new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/scheduler/app.yaml b/scheduler/app.yaml new file mode 100644 index 000000000000..8afa34736dad --- /dev/null +++ b/scheduler/app.yaml @@ -0,0 +1,18 @@ +# Copyright 2019 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# [START cloud_scheduler_python_yaml] +runtime: python37 +service: my-service +# [END cloud_scheduler_python_yaml] diff --git a/scheduler/create_job.py b/scheduler/create_job.py new file mode 100644 index 000000000000..f5025ca1a349 --- /dev/null +++ b/scheduler/create_job.py @@ -0,0 +1,77 @@ +# Copyright 2019 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +def create_scheduler_job(project_id, location_id, service_id): + """Create a job with an App Engine target via the Cloud Scheduler API""" + # [START cloud_scheduler_create_job] + from google.cloud import scheduler + + # Create a client. + client = scheduler.CloudSchedulerClient() + + # TODO(developer): Uncomment and set the following variables + # project_id = 'PROJECT_ID' + # location_id = 'LOCATION_ID' + # service_id = 'my-service' + + # Construct the fully qualified location path. + parent = client.location_path(project_id, location_id) + + # Construct the request body. + job = { + 'app_engine_http_target': { + 'app_engine_routing': { + 'service': service_id + }, + 'relative_uri': '/log_payload', + 'http_method': 'POST', + 'body': 'Hello World'.encode() + }, + 'schedule': '* * * * *', + 'time_zone': 'America/Los_Angeles' + } + + # Use the client to send the job creation request. + response = client.create_job(parent, job) + + print('Created job: {}'.format(response.name)) + # [END cloud_scheduler_create_job] + return response + + +def delete_scheduler_job(project_id, location_id, job_id): + """Delete a job via the Cloud Scheduler API""" + # [START cloud_scheduler_delete_job] + from google.cloud import scheduler + from google.api_core.exceptions import GoogleAPICallError + + # Create a client. + client = scheduler.CloudSchedulerClient() + + # TODO(developer): Uncomment and set the following variables + # project_id = 'PROJECT_ID' + # location_id = 'LOCATION_ID' + # job_id = 'JOB_ID' + + # Construct the fully qualified job path. + job = client.job_path(project_id, location_id, job_id) + + # Use the client to send the job deletion request. + try: + client.delete_job(job) + print("Job deleted.") + except GoogleAPICallError as e: + print("Error: %s" % e) + # [END cloud_scheduler_delete_job] diff --git a/scheduler/create_job_test.py b/scheduler/create_job_test.py new file mode 100644 index 000000000000..ab03c1b7629d --- /dev/null +++ b/scheduler/create_job_test.py @@ -0,0 +1,33 @@ +# Copyright 2019 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os + +import create_job + +TEST_PROJECT_ID = os.getenv('GCLOUD_PROJECT') +TEST_LOCATION = os.getenv('LOCATION_ID', 'us-central1') + + +def test_create_job(capsys): + create_result = create_job.create_scheduler_job( + TEST_PROJECT_ID, TEST_LOCATION, 'my-service') + out, _ = capsys.readouterr() + assert 'Created job:' in out + + job_name = create_result.name.split('/')[-1] + create_job.delete_scheduler_job(TEST_PROJECT_ID, TEST_LOCATION, job_name) + + out, _ = capsys.readouterr() + assert 'Job deleted.' in out diff --git a/scheduler/main.py b/scheduler/main.py new file mode 100644 index 000000000000..9d4d97537d27 --- /dev/null +++ b/scheduler/main.py @@ -0,0 +1,42 @@ +# Copyright 2019 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""App Engine app to serve as an endpoint for Cloud Scheduler samples.""" + +# [START cloud_scheduler_app] +from flask import Flask, request + +app = Flask(__name__) + + +# Define relative URI for job endpoint +@app.route('/log_payload', methods=['POST']) +def example_task_handler(): + """Log the job payload.""" + payload = request.get_data(as_text=True) or '(empty payload)' + print('Received job with payload: {}'.format(payload)) + return 'Printed job payload: {}'.format(payload) +# [END cloud_scheduler_app] + + +@app.route('/') +def hello(): + """Basic index to verify app is serving.""" + return 'Hello World!' + + +if __name__ == '__main__': + # This is used when running locally. Gunicorn is used to run the + # application on Google App Engine. See entrypoint in app.yaml. + app.run(host='127.0.0.1', port=8080, debug=True) diff --git a/scheduler/main_test.py b/scheduler/main_test.py new file mode 100644 index 000000000000..3d6745a5605c --- /dev/null +++ b/scheduler/main_test.py @@ -0,0 +1,45 @@ +# Copyright 2019 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + + +@pytest.fixture +def app(): + import main + main.app.testing = True + return main.app.test_client() + + +def test_index(app): + r = app.get('/') + assert r.status_code == 200 + + +def test_log_payload(capsys, app): + payload = 'test_payload' + + r = app.post('/log_payload', data=payload) + assert r.status_code == 200 + + out, _ = capsys.readouterr() + assert payload in out + + +def test_empty_payload(capsys, app): + r = app.post('/log_payload') + assert r.status_code == 200 + + out, _ = capsys.readouterr() + assert 'empty payload' in out diff --git a/scheduler/requirements.txt b/scheduler/requirements.txt new file mode 100644 index 000000000000..6fc789799aab --- /dev/null +++ b/scheduler/requirements.txt @@ -0,0 +1,3 @@ +Flask==1.0.2 +gunicorn==19.9.0 +google-cloud-scheduler==0.1.0