├── .dockerignore
├── .flaskenv
├── .gitattributes
├── .gitignore
├── .vscode
├── launch.json
└── settings.json
├── Dockerfile
├── LICENSE
├── Procfile
├── README.md
├── labs
├── 01_base_pipeline
│ ├── README.md
│ ├── pipeline.yaml
│ └── tasks.yaml
├── 02_add_git_trigger
│ ├── README.md
│ ├── eventlistener.yaml
│ ├── pipeline.yaml
│ ├── tasks.yaml
│ ├── triggerbinding.yaml
│ └── triggertemplate.yaml
├── 03_use_tekton_catalog
│ ├── README.md
│ ├── pipeline.yaml
│ ├── pvc.yaml
│ └── tasks.yaml
├── 04_unit_test_automation
│ ├── README.md
│ ├── pipeline.yaml
│ ├── pvc.yaml
│ └── tasks.yaml
├── 05_build_an_image
│ ├── README.md
│ ├── pipeline.yaml
│ ├── pvc.yaml
│ └── tasks.yaml
└── 06_deploy_to_kubernetes
│ ├── README.md
│ ├── pipeline.yaml
│ ├── pvc.yaml
│ └── tasks.yaml
├── requirements.txt
├── service
├── __init__.py
├── common
│ ├── error_handlers.py
│ ├── log_handlers.py
│ └── status.py
└── routes.py
├── setup.cfg
└── tests
└── test_routes.py
/.dockerignore:
--------------------------------------------------------------------------------
1 | # Files we don't want in our Docker image
2 | .git/
3 | lessons/
4 | Vagrantfile
5 | Dockerfile
6 |
7 | # Local environment
8 | .DS_Store
9 | Thumbs.db
10 |
11 | # Vagrant
12 | .vagrant/
13 |
14 | # Test reports
15 | unittests.xml
16 |
17 | # database files
18 | db/*
19 | !db/.keep
20 |
21 | # SonarQube Reports
22 | .scannerwork/
23 | .sonarlint/
24 |
25 | # Byte-compiled / optimized / DLL files
26 | __pycache__/
27 | *.py[cod]
28 | *$py.class
29 |
30 | # C extensions
31 | *.so
32 |
33 | # Distribution / packaging
34 | .Python
35 | env/
36 | build/
37 | develop-eggs/
38 | dist/
39 | downloads/
40 | eggs/
41 | .eggs/
42 | lib/
43 | lib64/
44 | parts/
45 | sdist/
46 | var/
47 | *.egg-info/
48 | .installed.cfg
49 | *.egg
50 |
51 | # PyInstaller
52 | # Usually these files are written by a python script from a template
53 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
54 | *.manifest
55 | *.spec
56 |
57 | # Installer logs
58 | pip-log.txt
59 | pip-delete-this-directory.txt
60 |
61 | # Unit test / coverage reports
62 | htmlcov/
63 | .tox/
64 | .coverage
65 | .coverage.*
66 | .cache
67 | nosetests.xml
68 | coverage.xml
69 | *,cover
70 | .hypothesis/
71 |
72 | # Translations
73 | *.mo
74 | *.pot
75 |
76 | # Django stuff:
77 | *.log
78 | local_settings.py
79 |
80 | # Flask stuff:
81 | instance/
82 | .webassets-cache
83 |
84 | # Scrapy stuff:
85 | .scrapy
86 |
87 | # Sphinx documentation
88 | docs/_build/
89 |
90 | # PyBuilder
91 | target/
92 |
93 | # IPython Notebook
94 | .ipynb_checkpoints
95 |
96 | # pyenv
97 | .python-version
98 |
99 | # celery beat schedule file
100 | celerybeat-schedule
101 |
102 | # dotenv
103 | .env
104 |
105 | # virtualenv
106 | venv/
107 | ENV/
108 |
109 | # Spyder project settings
110 | .spyderproject
111 |
112 | # Rope project settings
113 | .ropeproject
114 |
--------------------------------------------------------------------------------
/.flaskenv:
--------------------------------------------------------------------------------
1 | FLASK_RUN_PORT=8000
2 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto eol=lf
2 | *.{cmd,[cC][mM][dD]} text eol=crlf
3 | *.{bat,[bB][aA][tT]} text eol=crlf
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Pesky OS metadata files
2 | .DS_Store
3 | .Thumbs.db
4 |
5 | # Unit test results
6 | unittests.xml
7 |
8 | # Ignore ideas
9 | .ideas/
10 |
11 | # Vagrant metadata
12 | .vagrant/
13 |
14 | # Byte-compiled / optimized / DLL files
15 | __pycache__/
16 | *.py[cod]
17 | *$py.class
18 |
19 | # C extensions
20 | *.so
21 |
22 | # Distribution / packaging
23 | .Python
24 | build/
25 | develop-eggs/
26 | dist/
27 | downloads/
28 | eggs/
29 | .eggs/
30 | lib/
31 | lib64/
32 | parts/
33 | sdist/
34 | var/
35 | wheels/
36 | pip-wheel-metadata/
37 | share/python-wheels/
38 | *.egg-info/
39 | .installed.cfg
40 | *.egg
41 | MANIFEST
42 |
43 | # PyInstaller
44 | # Usually these files are written by a python script from a template
45 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
46 | *.manifest
47 | *.spec
48 |
49 | # Installer logs
50 | pip-log.txt
51 | pip-delete-this-directory.txt
52 |
53 | # Unit test / coverage reports
54 | htmlcov/
55 | .tox/
56 | .nox/
57 | .coverage
58 | .coverage.*
59 | .cache
60 | nosetests.xml
61 | coverage.xml
62 | *.cover
63 | *.py,cover
64 | .hypothesis/
65 | .pytest_cache/
66 |
67 | # Translations
68 | *.mo
69 | *.pot
70 |
71 | # Django stuff:
72 | *.log
73 | local_settings.py
74 | db.sqlite3
75 | db.sqlite3-journal
76 |
77 | # Flask stuff:
78 | instance/
79 | .webassets-cache
80 |
81 | # Scrapy stuff:
82 | .scrapy
83 |
84 | # Sphinx documentation
85 | docs/_build/
86 |
87 | # PyBuilder
88 | target/
89 |
90 | # Jupyter Notebook
91 | .ipynb_checkpoints
92 |
93 | # IPython
94 | profile_default/
95 | ipython_config.py
96 |
97 | # pyenv
98 | .python-version
99 |
100 | # pipenv
101 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
102 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
103 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
104 | # install all needed dependencies.
105 | #Pipfile.lock
106 |
107 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
108 | __pypackages__/
109 |
110 | # Celery stuff
111 | celerybeat-schedule
112 | celerybeat.pid
113 |
114 | # SageMath parsed files
115 | *.sage.py
116 |
117 | # Environments
118 | .env
119 | .venv
120 | env/
121 | venv/
122 | ENV/
123 | env.bak/
124 | venv.bak/
125 |
126 | # Spyder project settings
127 | .spyderproject
128 | .spyproject
129 |
130 | # Rope project settings
131 | .ropeproject
132 |
133 | # mkdocs documentation
134 | /site
135 |
136 | # mypy
137 | .mypy_cache/
138 | .dmypy.json
139 | dmypy.json
140 |
141 | # Pyre type checker
142 | .pyre/
143 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "configurations": [
3 | {
4 | "name": "Python: Flask",
5 | "type": "python",
6 | "request": "launch",
7 | "module": "flask",
8 | "env": {
9 | "FLASK_APP": "service:app",
10 | "FLASK_ENV": "development"
11 | },
12 | "args": [
13 | "run",
14 | "--no-debugger"
15 | ],
16 | "jinja": true,
17 | "justMyCode": true
18 | }
19 | ]
20 | },
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "markdown-preview-github-styles.colorTheme": "light",
3 | "makefile.extensionOutputFolder": "./.vscode",
4 | "git.mergeEditor": true,
5 | "python.pythonPath": "/usr/local/bin/python",
6 | "python.linting.enabled": true,
7 | "python.linting.pylintEnabled": true,
8 | "python.testing.nosetestsEnabled": true,
9 | "python.testing.nosetestArgs": ["-c=setup.cfg"],
10 | "python.testing.pytestEnabled": false,
11 | "python.testing.unittestEnabled": true,
12 | "python.testing.unittestArgs": [
13 | "-v",
14 | "-s",
15 | "./tests",
16 | "-p",
17 | "test*.py"
18 | ],
19 | "files.exclude": {
20 | "**/.git": true,
21 | "**/.svn": true,
22 | "**/.hg": true,
23 | "**/CVS": true,
24 | "**/.DS_Store": true,
25 | "**/*.pyc": true,
26 | "**/__pycache__": true
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.9-slim
2 |
3 | # Establish a working folder
4 | WORKDIR /app
5 |
6 | # Establish dependencies
7 | COPY requirements.txt .
8 | RUN python -m pip install -U pip wheel && \
9 | pip install -r requirements.txt
10 |
11 | # Copy source files last because they change the most
12 | COPY service ./service
13 |
14 | # Become non-root user
15 | RUN useradd -m -r service && \
16 | chown -R service:service /app
17 | USER service
18 |
19 | # Run the service on port 8000
20 | ENV PORT 8000
21 | EXPOSE $PORT
22 | CMD ["gunicorn", "service:app", "--bind", "0.0.0.0:8000"]
23 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: gunicorn --log-file=- --workers=1 --bind=0.0.0.0:$PORT service:app
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Intro to CI/CD Practice Code
2 |
3 | [](https://opensource.org/licenses/Apache-2.0)
4 | [](https://shields.io/)
5 |
6 | This repository contains the practice code for the labs in **IBM-CD0215EN-SkillsNetwork Introduction to CI/CD**
7 |
8 | ## Contents
9 |
10 | - Lab 1: [Build an empty Pipeline](labs/01_base_pipeline/README.md)
11 | - Lab 2: [Adding GitHub Triggers](labs/02_add_git_trigger/README.md)
12 | - Lab 3: [Use Tekton CD Catalog](labs/03_use_tekton_catalog/README.md)
13 | - Lab 4: [Integrate Unit Test Automation](labs/04_unit_test_automation/README.md)
14 | - Lab 5: [Building an Image](labs/05_build_an_image/README.md)
15 | - Lab 6: [Deploy to Kubernetes](labs/06_deploy_to_kubernetes/README.md)
16 |
17 | ## Instructor
18 |
19 | John Rofrano, Senior Technical Staff Member, DevOps Champion, @ IBM Research
20 |
21 | ##
© IBM Corporation 2022. All rights reserved.
--------------------------------------------------------------------------------
/labs/01_base_pipeline/README.md:
--------------------------------------------------------------------------------
1 | # Create a base pipeline
2 |
3 | This folder holds the files for the lab _Create a Base Pipeline_ which is part of the **IBM-CD0215EN-Skills Network Introduction to CI/CD** course.
4 |
--------------------------------------------------------------------------------
/labs/01_base_pipeline/pipeline.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: tekton.dev/v1beta1
2 | kind: Pipeline
3 | metadata:
4 | name:
5 | spec:
6 | tasks:
7 |
--------------------------------------------------------------------------------
/labs/01_base_pipeline/tasks.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: tekton.dev/v1beta1
2 | kind: Task
3 | metadata:
4 | name:
5 | spec:
6 | steps:
7 |
--------------------------------------------------------------------------------
/labs/02_add_git_trigger/README.md:
--------------------------------------------------------------------------------
1 | # Adding GitHub Triggers
2 |
3 | This folder holds the files for the lab: _Adding GitHub Triggers_ which is part of the **IBM-CD0215EN-Skills Network Introduction to CI/CD** course.
4 |
--------------------------------------------------------------------------------
/labs/02_add_git_trigger/eventlistener.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: triggers.tekton.dev/v1beta1
2 | kind: EventListener
3 | metadata:
4 | name:
5 | spec:
6 |
--------------------------------------------------------------------------------
/labs/02_add_git_trigger/pipeline.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: tekton.dev/v1beta1
2 | kind: Pipeline
3 | metadata:
4 | name: cd-pipeline
5 | spec:
6 | params:
7 | - name: repo-url
8 | - name: branch
9 | default: "master"
10 | tasks:
11 | - name: clone
12 | taskRef:
13 | name: checkout
14 | params:
15 | - name: repo-url
16 | value: "$(params.repo-url)"
17 | - name: branch
18 | value: "$(params.branch)"
19 |
20 | - name: lint
21 | taskRef:
22 | name: echo
23 | params:
24 | - name: message
25 | value: "Calling Flake8 linter..."
26 | runAfter:
27 | - clone
28 |
29 | - name: tests
30 | taskRef:
31 | name: echo
32 | params:
33 | - name: message
34 | value: "Running unit tests with PyUnit..."
35 | runAfter:
36 | - lint
37 |
38 | - name: build
39 | taskRef:
40 | name: echo
41 | params:
42 | - name: message
43 | value: "Building image for $(params.repo-url) ..."
44 | runAfter:
45 | - tests
46 |
47 | - name: deploy
48 | taskRef:
49 | name: echo
50 | params:
51 | - name: message
52 | value: "Deploying $(params.branch) branch of $(params.repo-url) ..."
53 | runAfter:
54 | - build
55 |
--------------------------------------------------------------------------------
/labs/02_add_git_trigger/tasks.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: tekton.dev/v1beta1
2 | kind: Task
3 | metadata:
4 | name: echo
5 | spec:
6 | params:
7 | - name: message
8 | description: The message to echo
9 | type: string
10 | steps:
11 | - name: echo-message
12 | image: alpine:3
13 | command: [/bin/echo]
14 | args: ["$(params.message)"]
15 |
16 | ---
17 | apiVersion: tekton.dev/v1beta1
18 | kind: Task
19 | metadata:
20 | name: checkout
21 | spec:
22 | params:
23 | - name: repo-url
24 | description: The URL of the git repo to clone
25 | type: string
26 | - name: branch
27 | description: The branch to clone
28 | type: string
29 | steps:
30 | - name: checkout
31 | image: bitnami/git:latest
32 | command: [git]
33 | args: ["clone", "--branch", "$(params.branch)", "$(params.repo-url)"]
34 |
--------------------------------------------------------------------------------
/labs/02_add_git_trigger/triggerbinding.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: triggers.tekton.dev/v1beta1
2 | kind: TriggerBinding
3 | metadata:
4 | name:
5 | spec:
6 |
--------------------------------------------------------------------------------
/labs/02_add_git_trigger/triggertemplate.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: triggers.tekton.dev/v1beta1
2 | kind: TriggerTemplate
3 | metadata:
4 | name:
5 | spec:
6 | params:
7 | # Add parameters here
8 | resourcetemplates:
9 | - apiVersion: tekton.dev/v1beta1
10 | kind: PipelineRun
11 | metadata:
12 | generateName: cd-pipeline-run-
13 | spec:
14 | # Add pipeline definition here
15 |
--------------------------------------------------------------------------------
/labs/03_use_tekton_catalog/README.md:
--------------------------------------------------------------------------------
1 | # Use Tekton CD Catalog
2 |
3 | This folder holds the files for the lab: _Use Tekton CD Catalog_ which is part of the **IBM-CD0215EN-Skills Network Introduction to CI/CD** course.
4 |
--------------------------------------------------------------------------------
/labs/03_use_tekton_catalog/pipeline.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: tekton.dev/v1beta1
2 | kind: Pipeline
3 | metadata:
4 | name: cd-pipeline
5 | spec:
6 | params:
7 | - name: repo-url
8 | - name: branch
9 | default: "master"
10 | tasks:
11 | - name: clone
12 | taskRef:
13 | name: checkout
14 | params:
15 | - name: repo-url
16 | value: "$(params.repo-url)"
17 | - name: branch
18 | value: "$(params.branch)"
19 |
20 | - name: lint
21 | taskRef:
22 | name: echo
23 | params:
24 | - name: message
25 | value: "Calling Flake8 linter..."
26 | runAfter:
27 | - clone
28 |
29 | - name: tests
30 | taskRef:
31 | name: echo
32 | params:
33 | - name: message
34 | value: "Running unit tests with PyUnit..."
35 | runAfter:
36 | - lint
37 |
38 | - name: build
39 | taskRef:
40 | name: echo
41 | params:
42 | - name: message
43 | value: "Building image for $(params.repo-url) ..."
44 | runAfter:
45 | - tests
46 |
47 | - name: deploy
48 | taskRef:
49 | name: echo
50 | params:
51 | - name: message
52 | value: "Deploying $(params.branch) branch of $(params.repo-url) ..."
53 | runAfter:
54 | - build
55 |
--------------------------------------------------------------------------------
/labs/03_use_tekton_catalog/pvc.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: PersistentVolumeClaim
3 | metadata:
4 | name: pipelinerun-pvc
5 | spec:
6 | storageClassName: skills-network-learner
7 | resources:
8 | requests:
9 | storage: 1Gi
10 | volumeMode: Filesystem
11 | accessModes:
12 | - ReadWriteOnce
13 |
--------------------------------------------------------------------------------
/labs/03_use_tekton_catalog/tasks.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: tekton.dev/v1beta1
2 | kind: Task
3 | metadata:
4 | name: echo
5 | spec:
6 | params:
7 | - name: message
8 | description: The message to echo
9 | type: string
10 | steps:
11 | - name: echo-message
12 | image: alpine:3
13 | command: [/bin/echo]
14 | args: ["$(params.message)"]
15 |
16 | ---
17 | apiVersion: tekton.dev/v1beta1
18 | kind: Task
19 | metadata:
20 | name: checkout
21 | spec:
22 | params:
23 | - name: repo-url
24 | description: The URL of the git repo to clone
25 | type: string
26 | - name: branch
27 | description: The branch to clone
28 | type: string
29 | steps:
30 | - name: checkout
31 | image: bitnami/git:latest
32 | command: [git]
33 | args: ["clone", "--branch", "$(params.branch)", "$(params.repo-url)"]
34 |
--------------------------------------------------------------------------------
/labs/04_unit_test_automation/README.md:
--------------------------------------------------------------------------------
1 | # Integrate Unit Test Automation
2 |
3 | This folder holds the files for the lab: _Integrate Unit Test Automation_ which is part of the **IBM-CD0215EN-Skills Network Introduction to CI/CD** course.
4 |
--------------------------------------------------------------------------------
/labs/04_unit_test_automation/pipeline.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: tekton.dev/v1beta1
2 | kind: Pipeline
3 | metadata:
4 | name: cd-pipeline
5 | spec:
6 | workspaces:
7 | - name: pipeline-workspace
8 | params:
9 | - name: repo-url
10 | - name: branch
11 | default: master
12 | tasks:
13 | - name: init
14 | workspaces:
15 | - name: source
16 | workspace: pipeline-workspace
17 | taskRef:
18 | name: cleanup
19 |
20 | - name: clone
21 | workspaces:
22 | - name: output
23 | workspace: pipeline-workspace
24 | taskRef:
25 | name: git-clone
26 | params:
27 | - name: url
28 | value: $(params.repo-url)
29 | - name: revision
30 | value: $(params.branch)
31 | runAfter:
32 | - init
33 |
34 | - name: lint
35 | taskRef:
36 | name: echo
37 | params:
38 | - name: message
39 | value: "Calling Flake8 linter..."
40 | runAfter:
41 | - clone
42 |
43 | - name: tests
44 | taskRef:
45 | name: echo
46 | params:
47 | - name: message
48 | value: "Running unit tests with PyUnit..."
49 | runAfter:
50 | - lint
51 |
52 | - name: build
53 | taskRef:
54 | name: echo
55 | params:
56 | - name: message
57 | value: "Building image for $(params.repo-url) ..."
58 | runAfter:
59 | - tests
60 |
61 | - name: deploy
62 | taskRef:
63 | name: echo
64 | params:
65 | - name: message
66 | value: "Deploying $(params.branch) branch of $(params.repo-url) ..."
67 | runAfter:
68 | - build
69 |
--------------------------------------------------------------------------------
/labs/04_unit_test_automation/pvc.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: PersistentVolumeClaim
3 | metadata:
4 | name: pipelinerun-pvc
5 | spec:
6 | storageClassName: skills-network-learner
7 | resources:
8 | requests:
9 | storage: 1Gi
10 | volumeMode: Filesystem
11 | accessModes:
12 | - ReadWriteOnce
13 |
--------------------------------------------------------------------------------
/labs/04_unit_test_automation/tasks.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: tekton.dev/v1beta1
2 | kind: Task
3 | metadata:
4 | name: echo
5 | spec:
6 | params:
7 | - name: message
8 | description: The message to echo
9 | type: string
10 | steps:
11 | - name: echo-message
12 | image: alpine:3
13 | command: [/bin/echo]
14 | args: ["$(params.message)"]
15 |
16 | ---
17 | apiVersion: tekton.dev/v1beta1
18 | kind: Task
19 | metadata:
20 | name: cleanup
21 | spec:
22 | description: This task will clean up a workspace by deleting all of the files.
23 | workspaces:
24 | - name: source
25 | steps:
26 | - name: remove
27 | image: alpine:3
28 | env:
29 | - name: WORKSPACE_SOURCE_PATH
30 | value: $(workspaces.source.path)
31 | workingDir: $(workspaces.source.path)
32 | securityContext:
33 | runAsNonRoot: false
34 | runAsUser: 0
35 | script: |
36 | #!/usr/bin/env sh
37 | set -eu
38 | echo "Removing all files from ${WORKSPACE_SOURCE_PATH} ..."
39 | # Delete any existing contents of the directory if it exists.
40 | #
41 | # We don't just "rm -rf ${WORKSPACE_SOURCE_PATH}" because ${WORKSPACE_SOURCE_PATH} might be "/"
42 | # or the root of a mounted volume.
43 | if [ -d "${WORKSPACE_SOURCE_PATH}" ] ; then
44 | # Delete non-hidden files and directories
45 | rm -rf "${WORKSPACE_SOURCE_PATH:?}"/*
46 | # Delete files and directories starting with . but excluding ..
47 | rm -rf "${WORKSPACE_SOURCE_PATH}"/.[!.]*
48 | # Delete files and directories starting with .. plus any other character
49 | rm -rf "${WORKSPACE_SOURCE_PATH}"/..?*
50 | fi
51 |
52 |
--------------------------------------------------------------------------------
/labs/05_build_an_image/README.md:
--------------------------------------------------------------------------------
1 | # Building an Image
2 |
3 | This folder holds the files for the lab: _Building an Image_ which is part of the **IBM-CD0215EN-Skills Network Introduction to CI/CD** course.
4 |
--------------------------------------------------------------------------------
/labs/05_build_an_image/pipeline.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: tekton.dev/v1beta1
2 | kind: Pipeline
3 | metadata:
4 | name: cd-pipeline
5 | spec:
6 | workspaces:
7 | - name: pipeline-workspace
8 | params:
9 | - name: repo-url
10 | - name: branch
11 | default: master
12 | tasks:
13 | - name: init
14 | workspaces:
15 | - name: source
16 | workspace: pipeline-workspace
17 | taskRef:
18 | name: cleanup
19 |
20 | - name: clone
21 | workspaces:
22 | - name: output
23 | workspace: pipeline-workspace
24 | taskRef:
25 | name: git-clone
26 | params:
27 | - name: url
28 | value: $(params.repo-url)
29 | - name: revision
30 | value: $(params.branch)
31 | runAfter:
32 | - init
33 |
34 | - name: lint
35 | workspaces:
36 | - name: source
37 | workspace: pipeline-workspace
38 | taskRef:
39 | name: flake8
40 | params:
41 | - name: image
42 | value: "python:3.9-slim"
43 | - name: args
44 | value: ["--count","--max-complexity=10","--max-line-length=127","--statistics"]
45 | runAfter:
46 | - clone
47 |
48 | - name: tests
49 | workspaces:
50 | - name: source
51 | workspace: pipeline-workspace
52 | taskRef:
53 | name: nose
54 | params:
55 | - name: args
56 | value: "-v --with-spec --spec-color"
57 | runAfter:
58 | - lint
59 |
60 | - name: build
61 | taskRef:
62 | name: echo
63 | params:
64 | - name: message
65 | value: "Building image for $(params.repo-url) ..."
66 | runAfter:
67 | - tests
68 |
69 | - name: deploy
70 | taskRef:
71 | name: echo
72 | params:
73 | - name: message
74 | value: "Deploying $(params.branch) branch of $(params.repo-url) ..."
75 | runAfter:
76 | - build
77 |
--------------------------------------------------------------------------------
/labs/05_build_an_image/pvc.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: PersistentVolumeClaim
3 | metadata:
4 | name: pipelinerun-pvc
5 | spec:
6 | storageClassName: skills-network-learner
7 | resources:
8 | requests:
9 | storage: 1Gi
10 | volumeMode: Filesystem
11 | accessModes:
12 | - ReadWriteOnce
13 |
--------------------------------------------------------------------------------
/labs/05_build_an_image/tasks.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: tekton.dev/v1beta1
2 | kind: Task
3 | metadata:
4 | name: echo
5 | spec:
6 | params:
7 | - name: message
8 | description: The message to echo
9 | type: string
10 | steps:
11 | - name: echo-message
12 | image: alpine:3
13 | command: [/bin/echo]
14 | args: ["$(params.message)"]
15 |
16 | ---
17 | apiVersion: tekton.dev/v1beta1
18 | kind: Task
19 | metadata:
20 | name: cleanup
21 | spec:
22 | description: This task will clean up a workspace by deleting all of the files.
23 | workspaces:
24 | - name: source
25 | steps:
26 | - name: remove
27 | image: alpine:3
28 | env:
29 | - name: WORKSPACE_SOURCE_PATH
30 | value: $(workspaces.source.path)
31 | workingDir: $(workspaces.source.path)
32 | securityContext:
33 | runAsNonRoot: false
34 | runAsUser: 0
35 | script: |
36 | #!/usr/bin/env sh
37 | set -eu
38 | echo "Removing all files from ${WORKSPACE_SOURCE_PATH} ..."
39 | # Delete any existing contents of the directory if it exists.
40 | #
41 | # We don't just "rm -rf ${WORKSPACE_SOURCE_PATH}" because ${WORKSPACE_SOURCE_PATH} might be "/"
42 | # or the root of a mounted volume.
43 | if [ -d "${WORKSPACE_SOURCE_PATH}" ] ; then
44 | # Delete non-hidden files and directories
45 | rm -rf "${WORKSPACE_SOURCE_PATH:?}"/*
46 | # Delete files and directories starting with . but excluding ..
47 | rm -rf "${WORKSPACE_SOURCE_PATH}"/.[!.]*
48 | # Delete files and directories starting with .. plus any other character
49 | rm -rf "${WORKSPACE_SOURCE_PATH}"/..?*
50 | fi
51 |
52 | ---
53 | apiVersion: tekton.dev/v1beta1
54 | kind: Task
55 | metadata:
56 | name: nose
57 | spec:
58 | params:
59 | - name: args
60 | description: Arguments to pass to nose
61 | type: string
62 | default: "-v"
63 | workspaces:
64 | - name: source
65 | steps:
66 | - name: nosetests
67 | image: python:3.9-slim
68 | workingDir: $(workspaces.source.path)
69 | script: |
70 | #!/bin/bash
71 | set -e
72 |
73 | echo "***** Environment *****"
74 | python --version
75 | pwd
76 |
77 | echo "***** Installing dependencies *****"
78 | python -m pip install --upgrade pip wheel
79 | pip install -r requirements.txt
80 |
81 | echo "***** Running nosetests with: $(params.args)"
82 | nosetests $(params.args)
83 |
--------------------------------------------------------------------------------
/labs/06_deploy_to_kubernetes/README.md:
--------------------------------------------------------------------------------
1 | # Deploy to Kubernetes / OpenShift
2 |
3 | This folder holds the files for the lab: _Deploy to Kubernetes_ which is part of the **IBM-CD0215EN-Skills Network Introduction to CI/CD** course.
4 |
--------------------------------------------------------------------------------
/labs/06_deploy_to_kubernetes/pipeline.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: tekton.dev/v1beta1
2 | kind: Pipeline
3 | metadata:
4 | name: cd-pipeline
5 | spec:
6 | workspaces:
7 | - name: pipeline-workspace
8 | params:
9 | - name: build-image
10 | - name: repo-url
11 | - name: branch
12 | default: master
13 | tasks:
14 | - name: init
15 | workspaces:
16 | - name: source
17 | workspace: pipeline-workspace
18 | taskRef:
19 | name: cleanup
20 |
21 | - name: clone
22 | workspaces:
23 | - name: output
24 | workspace: pipeline-workspace
25 | taskRef:
26 | name: git-clone
27 | params:
28 | - name: url
29 | value: $(params.repo-url)
30 | - name: revision
31 | value: $(params.branch)
32 | runAfter:
33 | - init
34 |
35 | - name: lint
36 | workspaces:
37 | - name: source
38 | workspace: pipeline-workspace
39 | taskRef:
40 | name: flake8
41 | params:
42 | - name: image
43 | value: "python:3.9-slim"
44 | - name: args
45 | value: ["--count","--max-complexity=10","--max-line-length=127","--statistics"]
46 | runAfter:
47 | - clone
48 |
49 | - name: tests
50 | workspaces:
51 | - name: source
52 | workspace: pipeline-workspace
53 | taskRef:
54 | name: nose
55 | params:
56 | - name: args
57 | value: "-v --with-spec --spec-color"
58 | runAfter:
59 | - lint
60 |
61 | - name: build
62 | workspaces:
63 | - name: source
64 | workspace: pipeline-workspace
65 | taskRef:
66 | name: buildah
67 | kind: ClusterTask
68 | params:
69 | - name: IMAGE
70 | value: "$(params.build-image)"
71 | runAfter:
72 | - tests
73 |
74 | - name: deploy
75 | taskRef:
76 | name: echo
77 | params:
78 | - name: message
79 | value: "Deploying $(params.branch) branch of $(params.repo-url) ..."
80 | runAfter:
81 | - build
82 |
--------------------------------------------------------------------------------
/labs/06_deploy_to_kubernetes/pvc.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: PersistentVolumeClaim
3 | metadata:
4 | name: pipelinerun-pvc
5 | spec:
6 | storageClassName: skills-network-learner
7 | resources:
8 | requests:
9 | storage: 1Gi
10 | volumeMode: Filesystem
11 | accessModes:
12 | - ReadWriteOnce
13 |
--------------------------------------------------------------------------------
/labs/06_deploy_to_kubernetes/tasks.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: tekton.dev/v1beta1
2 | kind: Task
3 | metadata:
4 | name: echo
5 | spec:
6 | params:
7 | - name: message
8 | description: The message to echo
9 | type: string
10 | steps:
11 | - name: echo-message
12 | image: alpine:3
13 | command: [/bin/echo]
14 | args: ["$(params.message)"]
15 |
16 | ---
17 | apiVersion: tekton.dev/v1beta1
18 | kind: Task
19 | metadata:
20 | name: cleanup
21 | spec:
22 | description: This task will clean up a workspace by deleting all of the files.
23 | workspaces:
24 | - name: source
25 | steps:
26 | - name: remove
27 | image: alpine:3
28 | env:
29 | - name: WORKSPACE_SOURCE_PATH
30 | value: $(workspaces.source.path)
31 | workingDir: $(workspaces.source.path)
32 | securityContext:
33 | runAsNonRoot: false
34 | runAsUser: 0
35 | script: |
36 | #!/usr/bin/env sh
37 | set -eu
38 | echo "Removing all files from ${WORKSPACE_SOURCE_PATH} ..."
39 | # Delete any existing contents of the directory if it exists.
40 | #
41 | # We don't just "rm -rf ${WORKSPACE_SOURCE_PATH}" because ${WORKSPACE_SOURCE_PATH} might be "/"
42 | # or the root of a mounted volume.
43 | if [ -d "${WORKSPACE_SOURCE_PATH}" ] ; then
44 | # Delete non-hidden files and directories
45 | rm -rf "${WORKSPACE_SOURCE_PATH:?}"/*
46 | # Delete files and directories starting with . but excluding ..
47 | rm -rf "${WORKSPACE_SOURCE_PATH}"/.[!.]*
48 | # Delete files and directories starting with .. plus any other character
49 | rm -rf "${WORKSPACE_SOURCE_PATH}"/..?*
50 | fi
51 |
52 | ---
53 | apiVersion: tekton.dev/v1beta1
54 | kind: Task
55 | metadata:
56 | name: nose
57 | spec:
58 | params:
59 | - name: args
60 | description: Arguments to pass to nose
61 | type: string
62 | default: "-v"
63 | workspaces:
64 | - name: source
65 | steps:
66 | - name: nosetests
67 | image: python:3.9-slim
68 | workingDir: $(workspaces.source.path)
69 | script: |
70 | #!/bin/bash
71 | set -e
72 |
73 | echo "***** Environment *****"
74 | python --version
75 | pwd
76 |
77 | echo "***** Installing dependencies *****"
78 | python -m pip install --upgrade pip wheel
79 | pip install -r requirements.txt
80 |
81 | echo "***** Running nosetests with: $(params.args)"
82 | nosetests $(params.args)
83 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | # Werkzeug keeps breaking Flask!
2 | Werkzeug==2.1.2
3 |
4 | # Runtime dependencies
5 | Flask==2.1.2
6 | python-dotenv==0.20.0
7 |
8 | # Runtime tools
9 | gunicorn==20.1.0
10 | honcho==1.1.0
11 |
12 | # Code quality
13 | pylint==2.14.0
14 | flake8==4.0.1
15 | black==22.3.0
16 |
17 | # Testing dependencies
18 | nose==1.3.7
19 | pinocchio==0.4.3
20 | coverage==6.3.2
21 |
22 | # Utilities
23 | httpie==3.2.1
24 |
--------------------------------------------------------------------------------
/service/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | Service Package
3 | """
4 | from flask import Flask
5 |
6 | app = Flask(__name__)
7 |
8 | # This must be imported after the Flask app is created
9 | from service import routes # pylint: disable=wrong-import-position,cyclic-import
10 | from service.common import log_handlers # pylint: disable=wrong-import-position
11 |
12 | log_handlers.init_logging(app, "gunicorn.error")
13 |
14 | app.logger.info(70 * "*")
15 | app.logger.info(" S E R V I C E R U N N I N G ".center(70, "*"))
16 | app.logger.info(70 * "*")
17 |
--------------------------------------------------------------------------------
/service/common/error_handlers.py:
--------------------------------------------------------------------------------
1 | ######################################################################
2 | # Copyright 2016, 2022 John J. Rofrano. 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 | # https://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 | Module: error_handlers
19 | """
20 | from flask import jsonify
21 | from service import app
22 | from . import status
23 |
24 |
25 | ######################################################################
26 | # Error Handlers
27 | ######################################################################
28 | @app.errorhandler(status.HTTP_400_BAD_REQUEST)
29 | def bad_request(error):
30 | """Handles bad requests with 400_BAD_REQUEST"""
31 | message = str(error)
32 | app.logger.warning(message)
33 | return (
34 | jsonify(
35 | status=status.HTTP_400_BAD_REQUEST, error="Bad Request", message=message
36 | ),
37 | status.HTTP_400_BAD_REQUEST,
38 | )
39 |
40 |
41 | @app.errorhandler(status.HTTP_404_NOT_FOUND)
42 | def not_found(error):
43 | """Handles resources not found with 404_NOT_FOUND"""
44 | message = str(error)
45 | app.logger.warning(message)
46 | return (
47 | jsonify(status=status.HTTP_404_NOT_FOUND, error="Not Found", message=message),
48 | status.HTTP_404_NOT_FOUND,
49 | )
50 |
51 |
52 | @app.errorhandler(status.HTTP_405_METHOD_NOT_ALLOWED)
53 | def method_not_supported(error):
54 | """Handles unsupported HTTP methods with 405_METHOD_NOT_SUPPORTED"""
55 | message = str(error)
56 | app.logger.warning(message)
57 | return (
58 | jsonify(
59 | status=status.HTTP_405_METHOD_NOT_ALLOWED,
60 | error="Method not Allowed",
61 | message=message,
62 | ),
63 | status.HTTP_405_METHOD_NOT_ALLOWED,
64 | )
65 |
66 |
67 | @app.errorhandler(status.HTTP_409_CONFLICT)
68 | def resource_conflict(error):
69 | """Handles resource conflicts with HTTP_409_CONFLICT"""
70 | message = str(error)
71 | app.logger.warning(message)
72 | return (
73 | jsonify(
74 | status=status.HTTP_409_CONFLICT,
75 | error="Conflict",
76 | message=message,
77 | ),
78 | status.HTTP_409_CONFLICT,
79 | )
80 |
81 |
82 | @app.errorhandler(status.HTTP_415_UNSUPPORTED_MEDIA_TYPE)
83 | def mediatype_not_supported(error):
84 | """Handles unsupported media requests with 415_UNSUPPORTED_MEDIA_TYPE"""
85 | message = str(error)
86 | app.logger.warning(message)
87 | return (
88 | jsonify(
89 | status=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
90 | error="Unsupported media type",
91 | message=message,
92 | ),
93 | status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
94 | )
95 |
96 |
97 | @app.errorhandler(status.HTTP_500_INTERNAL_SERVER_ERROR)
98 | def internal_server_error(error):
99 | """Handles unexpected server error with 500_SERVER_ERROR"""
100 | message = str(error)
101 | app.logger.error(message)
102 | return (
103 | jsonify(
104 | status=status.HTTP_500_INTERNAL_SERVER_ERROR,
105 | error="Internal Server Error",
106 | message=message,
107 | ),
108 | status.HTTP_500_INTERNAL_SERVER_ERROR,
109 | )
110 |
--------------------------------------------------------------------------------
/service/common/log_handlers.py:
--------------------------------------------------------------------------------
1 | ######################################################################
2 | # Copyright 2016, 2022 John J. Rofrano. 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 | # https://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 | Log Handlers
19 |
20 | This module contains utility functions to set up logging
21 | consistently
22 | """
23 | import logging
24 |
25 |
26 | def init_logging(app, logger_name: str):
27 | """Set up logging for production"""
28 | app.logger.propagate = False
29 | gunicorn_logger = logging.getLogger(logger_name)
30 | app.logger.handlers = gunicorn_logger.handlers
31 | app.logger.setLevel(gunicorn_logger.level)
32 | # Make all log formats consistent
33 | formatter = logging.Formatter(
34 | "[%(asctime)s] [%(levelname)s] [%(module)s] %(message)s", "%Y-%m-%d %H:%M:%S %z"
35 | )
36 | for handler in app.logger.handlers:
37 | handler.setFormatter(formatter)
38 | app.logger.info("Logging handler established")
39 |
--------------------------------------------------------------------------------
/service/common/status.py:
--------------------------------------------------------------------------------
1 | """
2 | Descriptive HTTP status codes, for code readability.
3 | See RFC 2616 and RFC 6585.
4 | RFC 2616: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
5 | RFC 6585: http://tools.ietf.org/html/rfc6585
6 | """
7 |
8 | # Informational - 1xx
9 | HTTP_100_CONTINUE = 100
10 | HTTP_101_SWITCHING_PROTOCOLS = 101
11 |
12 | # Successful - 2xx
13 | HTTP_200_OK = 200
14 | HTTP_201_CREATED = 201
15 | HTTP_202_ACCEPTED = 202
16 | HTTP_203_NON_AUTHORITATIVE_INFORMATION = 203
17 | HTTP_204_NO_CONTENT = 204
18 | HTTP_205_RESET_CONTENT = 205
19 | HTTP_206_PARTIAL_CONTENT = 206
20 |
21 | # Redirection - 3xx
22 | HTTP_300_MULTIPLE_CHOICES = 300
23 | HTTP_301_MOVED_PERMANENTLY = 301
24 | HTTP_302_FOUND = 302
25 | HTTP_303_SEE_OTHER = 303
26 | HTTP_304_NOT_MODIFIED = 304
27 | HTTP_305_USE_PROXY = 305
28 | HTTP_306_RESERVED = 306
29 | HTTP_307_TEMPORARY_REDIRECT = 307
30 |
31 | # Client Error - 4xx
32 | HTTP_400_BAD_REQUEST = 400
33 | HTTP_401_UNAUTHORIZED = 401
34 | HTTP_402_PAYMENT_REQUIRED = 402
35 | HTTP_403_FORBIDDEN = 403
36 | HTTP_404_NOT_FOUND = 404
37 | HTTP_405_METHOD_NOT_ALLOWED = 405
38 | HTTP_406_NOT_ACCEPTABLE = 406
39 | HTTP_407_PROXY_AUTHENTICATION_REQUIRED = 407
40 | HTTP_408_REQUEST_TIMEOUT = 408
41 | HTTP_409_CONFLICT = 409
42 | HTTP_410_GONE = 410
43 | HTTP_411_LENGTH_REQUIRED = 411
44 | HTTP_412_PRECONDITION_FAILED = 412
45 | HTTP_413_REQUEST_ENTITY_TOO_LARGE = 413
46 | HTTP_414_REQUEST_URI_TOO_LONG = 414
47 | HTTP_415_UNSUPPORTED_MEDIA_TYPE = 415
48 | HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE = 416
49 | HTTP_417_EXPECTATION_FAILED = 417
50 | HTTP_428_PRECONDITION_REQUIRED = 428
51 | HTTP_429_TOO_MANY_REQUESTS = 429
52 | HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE = 431
53 |
54 | # Server Error - 5xx
55 | HTTP_500_INTERNAL_SERVER_ERROR = 500
56 | HTTP_501_NOT_IMPLEMENTED = 501
57 | HTTP_502_BAD_GATEWAY = 502
58 | HTTP_503_SERVICE_UNAVAILABLE = 503
59 | HTTP_504_GATEWAY_TIMEOUT = 504
60 | HTTP_505_HTTP_VERSION_NOT_SUPPORTED = 505
61 | HTTP_511_NETWORK_AUTHENTICATION_REQUIRED = 511
62 |
--------------------------------------------------------------------------------
/service/routes.py:
--------------------------------------------------------------------------------
1 | """
2 | Controller for routes
3 | """
4 | from flask import jsonify, url_for, abort
5 | from service import app
6 | from service.common import status
7 |
8 | COUNTER = {}
9 |
10 |
11 | ############################################################
12 | # Health Endpoint
13 | ############################################################
14 | @app.route("/health")
15 | def health():
16 | """Health Status"""
17 | return jsonify(dict(status="OK")), status.HTTP_200_OK
18 |
19 |
20 | ############################################################
21 | # Index page
22 | ############################################################
23 | @app.route("/")
24 | def index():
25 | """Returns information abut the service"""
26 | app.logger.info("Request for Base URL")
27 | return jsonify(
28 | status=status.HTTP_200_OK,
29 | message="Hit Counter Service",
30 | version="1.0.0",
31 | url=url_for("list_counters", _external=True),
32 | )
33 |
34 |
35 | ############################################################
36 | # List counters
37 | ############################################################
38 | @app.route("/counters", methods=["GET"])
39 | def list_counters():
40 | """Lists all counters"""
41 | app.logger.info("Request to list all counters...")
42 |
43 | counters = [dict(name=count[0], counter=count[1]) for count in COUNTER.items()]
44 |
45 | return jsonify(counters)
46 |
47 |
48 | ############################################################
49 | # Create counters
50 | ############################################################
51 | @app.route("/counters/", methods=["POST"])
52 | def create_counters(name):
53 | """Creates a new counter"""
54 | app.logger.info("Request to Create counter: %s...", name)
55 |
56 | if name in COUNTER:
57 | return abort(status.HTTP_409_CONFLICT, f"Counter {name} already exists")
58 |
59 | COUNTER[name] = 0
60 |
61 | location_url = url_for("read_counters", name=name, _external=True)
62 | return (
63 | jsonify(name=name, counter=0),
64 | status.HTTP_201_CREATED,
65 | {"Location": location_url},
66 | )
67 |
68 |
69 | ############################################################
70 | # Read counters
71 | ############################################################
72 | @app.route("/counters/", methods=["GET"])
73 | def read_counters(name):
74 | """Reads a single counter"""
75 | app.logger.info("Request to Read counter: %s...", name)
76 |
77 | if name not in COUNTER:
78 | return abort(status.HTTP_404_NOT_FOUND, f"Counter {name} does not exist")
79 |
80 | counter = COUNTER[name]
81 | return jsonify(name=name, counter=counter)
82 |
83 |
84 | ############################################################
85 | # Update counters
86 | ############################################################
87 | @app.route("/counters/", methods=["PUT"])
88 | def update_counters(name):
89 | """Updates a counter"""
90 | app.logger.info("Request to Update counter: %s...", name)
91 |
92 | if name not in COUNTER:
93 | return abort(status.HTTP_404_NOT_FOUND, f"Counter {name} does not exist")
94 |
95 | COUNTER[name] += 1
96 |
97 | counter = COUNTER[name]
98 | return jsonify(name=name, counter=counter)
99 |
100 |
101 | ############################################################
102 | # Delete counters
103 | ############################################################
104 | @app.route("/counters/", methods=["DELETE"])
105 | def delete_counters(name):
106 | """Deletes a counter"""
107 | app.logger.info("Request to Delete counter: %s...", name)
108 |
109 | if name in COUNTER:
110 | COUNTER.pop(name)
111 |
112 | return "", status.HTTP_204_NO_CONTENT
113 |
114 |
115 | ############################################################
116 | # Utility for testing
117 | ############################################################
118 | def reset_counters():
119 | """Removes all counters while testing"""
120 | global COUNTER # pylint: disable=global-statement
121 | if app.testing:
122 | COUNTER = {}
123 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [nosetests]
2 | verbosity=2
3 | with-spec=1
4 | spec-color=1
5 | with-coverage=1
6 | cover-erase=1
7 | cover-package=service
8 | # cover-xml=1
9 | # cover-xml-file=./coverage.xml
10 | # with-xunit=1
11 | # xunit-file=./unittests.xml
12 |
13 | [coverage:report]
14 | show_missing = True
15 |
16 | [flake8]
17 | per-file-ignores =
18 | */__init__.py: F401 E402
19 |
20 | [pylint.'MESSAGES CONTROL']
21 | disable=E1101
22 |
--------------------------------------------------------------------------------
/tests/test_routes.py:
--------------------------------------------------------------------------------
1 | """
2 | Counter API Service Test Suite
3 |
4 | Test cases can be run with the following:
5 | nosetests -v --with-spec --spec-color
6 | coverage report -m
7 | """
8 | from unittest import TestCase
9 | from service.common import status # HTTP Status Codes
10 | from service.routes import app, reset_counters
11 |
12 |
13 | ######################################################################
14 | # T E S T C A S E S
15 | ######################################################################
16 | class CounterTest(TestCase):
17 | """ REST API Server Tests """
18 |
19 | @classmethod
20 | def setUpClass(cls):
21 | """ This runs once before the entire test suite """
22 | app.testing = True
23 |
24 | @classmethod
25 | def tearDownClass(cls):
26 | """ This runs once after the entire test suite """
27 | pass
28 |
29 | def setUp(self):
30 | """ This runs before each test """
31 | reset_counters()
32 | self.app = app.test_client()
33 |
34 | def tearDown(self):
35 | """ This runs after each test """
36 | pass
37 |
38 | ######################################################################
39 | # T E S T C A S E S
40 | ######################################################################
41 |
42 | def test_index(self):
43 | """ It should call the index call """
44 | resp = self.app.get("/")
45 | self.assertEqual(resp.status_code, status.HTTP_200_OK)
46 |
47 | def test_health(self):
48 | """ It should be healthy """
49 | resp = self.app.get("/health")
50 | self.assertEqual(resp.status_code, status.HTTP_200_OK)
51 |
52 | def test_create_counters(self):
53 | """ It should Create a counter """
54 | name = "foo"
55 | resp = self.app.post(f"/counters/{name}")
56 | self.assertEqual(resp.status_code, status.HTTP_201_CREATED)
57 | data = resp.get_json()
58 | self.assertEqual(data["name"], name)
59 | self.assertEqual(data["counter"], 0)
60 |
61 | def test_create_duplicate_counter(self):
62 | """ It should not Create a duplicate counter """
63 | name = "foo"
64 | resp = self.app.post(f"/counters/{name}")
65 | self.assertEqual(resp.status_code, status.HTTP_201_CREATED)
66 | data = resp.get_json()
67 | self.assertEqual(data["name"], name)
68 | self.assertEqual(data["counter"], 0)
69 | resp = self.app.post(f"/counters/{name}")
70 | self.assertEqual(resp.status_code, status.HTTP_409_CONFLICT)
71 |
72 | def test_list_counters(self):
73 | """ It should List counters """
74 | resp = self.app.get("/counters")
75 | self.assertEqual(resp.status_code, status.HTTP_200_OK)
76 | data = resp.get_json()
77 | self.assertEqual(len(data), 0)
78 | # create a counter and name sure it appears in the list
79 | self.app.post("/counters/foo")
80 | resp = self.app.get("/counters")
81 | self.assertEqual(resp.status_code, status.HTTP_200_OK)
82 | data = resp.get_json()
83 | self.assertEqual(len(data), 1)
84 |
85 | def test_read_counters(self):
86 | """ It should Read a counter """
87 | name = "foo"
88 | self.app.post(f"/counters/{name}")
89 | resp = self.app.get(f"/counters/{name}")
90 | self.assertEqual(resp.status_code, status.HTTP_200_OK)
91 | data = resp.get_json()
92 | self.assertEqual(data["name"], name)
93 | self.assertEqual(data["counter"], 0)
94 |
95 | def test_update_counters(self):
96 | """ It should Update a counter """
97 | name = "foo"
98 | resp = self.app.post(f"/counters/{name}")
99 | self.assertEqual(resp.status_code, status.HTTP_201_CREATED)
100 | resp = self.app.get(f"/counters/{name}")
101 | self.assertEqual(resp.status_code, status.HTTP_200_OK)
102 | data = resp.get_json()
103 | print(data)
104 | self.assertEqual(data["name"], name)
105 | self.assertEqual(data["counter"], 0)
106 | # now update it
107 | resp = self.app.put(f"/counters/{name}")
108 | self.assertEqual(resp.status_code, status.HTTP_200_OK)
109 | data = resp.get_json()
110 | self.assertEqual(data["name"], name)
111 | self.assertEqual(data["counter"], 1)
112 |
113 | def test_update_missing_counters(self):
114 | """ It should not Update a missing counter """
115 | name = "foo"
116 | resp = self.app.put(f"/counters/{name}")
117 | self.assertEqual(resp.status_code, status.HTTP_404_NOT_FOUND)
118 |
119 | def test_delete_counters(self):
120 | """ It should Delete a counter """
121 | name = "foo"
122 | # Create a counter
123 | resp = self.app.post(f"/counters/{name}")
124 | self.assertEqual(resp.status_code, status.HTTP_201_CREATED)
125 | # Delete it twice should return the same
126 | resp = self.app.delete(f"/counters/{name}")
127 | self.assertEqual(resp.status_code, status.HTTP_204_NO_CONTENT)
128 | resp = self.app.delete(f"/counters/{name}")
129 | self.assertEqual(resp.status_code, status.HTTP_204_NO_CONTENT)
130 | # Gte it to make sure it's really gone
131 | resp = self.app.get(f"/counters/{name}")
132 | self.assertEqual(resp.status_code, status.HTTP_404_NOT_FOUND)
133 |
--------------------------------------------------------------------------------