├── .github
├── generate-toc
└── workflows
│ ├── e2e-test-deploy.yaml
│ ├── help-action.yml
│ └── slash-command-dispatcher.yaml
├── .gitignore
├── LICENSE
├── README.md
├── bentoctl_lambda
├── __init__.py
├── aws_lambda
│ ├── README.md
│ ├── app.py
│ ├── aws-lambda-rie-x86
│ ├── bentoml_server_config.yaml
│ ├── entry_script.sh
│ └── template.j2
├── create_deployable.py
├── generate.py
├── parameters.py
├── registry_utils.py
├── templates
│ ├── cloudformation_default.yaml
│ └── terraform_default.tf
└── utils.py
├── operator_config.py
├── requirements.txt
└── tests
├── classifier.py
└── test_api.py
/.github/generate-toc:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # This script was taken from https://github.com/ekalinin/github-markdown-toc
4 | # checkout the repo for how to use it
5 | #
6 | # Internal Working
7 | # Steps:
8 | #
9 | # 1. Download corresponding html file for some README.md:
10 | # curl -s $1
11 | #
12 | # 2. Discard rows where no substring 'user-content-' (github's markup):
13 | # awk '/user-content-/ { ...
14 | #
15 | # 3.1 Get last number in each row like ' ... sitemap.js.*<\/h/)+2, RLENGTH-5)
24 | #
25 | # 5. Find anchor and insert it inside "(...)":
26 | # substr($0, match($0, "href=\"[^\"]+?\" ")+6, RLENGTH-8)
27 | #
28 |
29 | gh_toc_version="0.8.0"
30 |
31 | gh_user_agent="gh-md-toc v$gh_toc_version"
32 |
33 | #
34 | # Download rendered into html README.md by its url.
35 | #
36 | #
37 | gh_toc_load() {
38 | local gh_url=$1
39 |
40 | if type curl &>/dev/null; then
41 | curl --user-agent "$gh_user_agent" -s "$gh_url"
42 | elif type wget &>/dev/null; then
43 | wget --user-agent="$gh_user_agent" -qO- "$gh_url"
44 | else
45 | echo "Please, install 'curl' or 'wget' and try again."
46 | exit 1
47 | fi
48 | }
49 |
50 | #
51 | # Converts local md file into html by GitHub
52 | #
53 | # -> curl -X POST --data '{"text": "Hello world github/linguist#1 **cool**, and #1!"}' https://api.github.com/markdown
54 | #
Hello world github/linguist#1 cool, and #1!
'"
55 | gh_toc_md2html() {
56 | local gh_file_md=$1
57 | URL=https://api.github.com/markdown/raw
58 |
59 | if [ ! -z "$GH_TOC_TOKEN" ]; then
60 | TOKEN=$GH_TOC_TOKEN
61 | else
62 | TOKEN_FILE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/token.txt"
63 | if [ -f "$TOKEN_FILE" ]; then
64 | TOKEN="$(cat $TOKEN_FILE)"
65 | fi
66 | fi
67 | if [ ! -z "${TOKEN}" ]; then
68 | AUTHORIZATION="Authorization: token ${TOKEN}"
69 | fi
70 |
71 | # echo $URL 1>&2
72 | OUTPUT=$(curl -s \
73 | --user-agent "$gh_user_agent" \
74 | --data-binary @"$gh_file_md" \
75 | -H "Content-Type:text/plain" \
76 | -H "$AUTHORIZATION" \
77 | "$URL")
78 |
79 | if [ "$?" != "0" ]; then
80 | echo "XXNetworkErrorXX"
81 | fi
82 | if [ "$(echo "${OUTPUT}" | awk '/API rate limit exceeded/')" != "" ]; then
83 | echo "XXRateLimitXX"
84 | else
85 | echo "${OUTPUT}"
86 | fi
87 | }
88 |
89 |
90 | #
91 | # Is passed string url
92 | #
93 | gh_is_url() {
94 | case $1 in
95 | https* | http*)
96 | echo "yes";;
97 | *)
98 | echo "no";;
99 | esac
100 | }
101 |
102 | #
103 | # TOC generator
104 | #
105 | gh_toc(){
106 | local gh_src=$1
107 | local gh_src_copy=$1
108 | local gh_ttl_docs=$2
109 | local need_replace=$3
110 | local no_backup=$4
111 | local no_footer=$5
112 |
113 | if [ "$gh_src" = "" ]; then
114 | echo "Please, enter URL or local path for a README.md"
115 | exit 1
116 | fi
117 |
118 |
119 | # Show "TOC" string only if working with one document
120 | if [ "$gh_ttl_docs" = "1" ]; then
121 |
122 | echo "Table of Contents"
123 | echo "================="
124 | echo ""
125 | gh_src_copy=""
126 |
127 | fi
128 |
129 | if [ "$(gh_is_url "$gh_src")" == "yes" ]; then
130 | gh_toc_load "$gh_src" | gh_toc_grab "$gh_src_copy"
131 | if [ "${PIPESTATUS[0]}" != "0" ]; then
132 | echo "Could not load remote document."
133 | echo "Please check your url or network connectivity"
134 | exit 1
135 | fi
136 | if [ "$need_replace" = "yes" ]; then
137 | echo
138 | echo "!! '$gh_src' is not a local file"
139 | echo "!! Can't insert the TOC into it."
140 | echo
141 | fi
142 | else
143 | local rawhtml=$(gh_toc_md2html "$gh_src")
144 | if [ "$rawhtml" == "XXNetworkErrorXX" ]; then
145 | echo "Parsing local markdown file requires access to github API"
146 | echo "Please make sure curl is installed and check your network connectivity"
147 | exit 1
148 | fi
149 | if [ "$rawhtml" == "XXRateLimitXX" ]; then
150 | echo "Parsing local markdown file requires access to github API"
151 | echo "Error: You exceeded the hourly limit. See: https://developer.github.com/v3/#rate-limiting"
152 | TOKEN_FILE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/token.txt"
153 | echo "or place GitHub auth token here: ${TOKEN_FILE}"
154 | exit 1
155 | fi
156 | local toc=`echo "$rawhtml" | gh_toc_grab "$gh_src_copy"`
157 | echo "$toc"
158 | if [ "$need_replace" = "yes" ]; then
159 | if grep -Fxq "" $gh_src && grep -Fxq "" $gh_src; then
160 | echo "Found markers"
161 | else
162 | echo "You don't have or in your file...exiting"
163 | exit 1
164 | fi
165 | local ts="<\!--ts-->"
166 | local te="<\!--te-->"
167 | local dt=`date +'%F_%H%M%S'`
168 | local ext=".orig.${dt}"
169 | local toc_path="${gh_src}.toc.${dt}"
170 | local toc_header="## Table of Contents"
171 | local toc_footer=""
172 | # http://fahdshariff.blogspot.ru/2012/12/sed-mutli-line-replacement-between-two.html
173 | # clear old TOC
174 | sed -i${ext} "/${ts}/,/${te}/{//!d;}" "$gh_src"
175 |
176 | # add header
177 | echo -e "\n${toc_header}\n" > "${toc_path}"
178 |
179 | # create toc file
180 | echo "${toc}" >> "$toc_path"
181 |
182 | # add footer
183 | if [ "${no_footer}" != "yes" ]; then
184 | echo -e "\n${toc_footer}\n" >> "$toc_path"
185 | fi
186 |
187 | # insert toc file
188 | if [[ "`uname`" == "Darwin" ]]; then
189 | sed -i "" "/${ts}/r ${toc_path}" "$gh_src"
190 | else
191 | sed -i "/${ts}/r ${toc_path}" "$gh_src"
192 | fi
193 | echo
194 | if [ "${no_backup}" = "yes" ]; then
195 | rm ${toc_path} ${gh_src}${ext}
196 | fi
197 | echo "!! TOC was added into: '$gh_src'"
198 | if [ -z "${no_backup}" ]; then
199 | echo "!! Origin version of the file: '${gh_src}${ext}'"
200 | echo "!! TOC added into a separate file: '${toc_path}'"
201 | fi
202 | echo
203 | fi
204 | fi
205 | }
206 |
207 | #
208 | # Grabber of the TOC from rendered html
209 | #
210 | # $1 - a source url of document.
211 | # It's need if TOC is generated for multiple documents.
212 | #
213 | gh_toc_grab() {
214 | common_awk_script='
215 | modified_href = ""
216 | split(href, chars, "")
217 | for (i=1;i <= length(href); i++) {
218 | c = chars[i]
219 | res = ""
220 | if (c == "+") {
221 | res = " "
222 | } else {
223 | if (c == "%") {
224 | res = "\\x"
225 | } else {
226 | res = c ""
227 | }
228 | }
229 | modified_href = modified_href res
230 | }
231 | print sprintf("%*s", (level-1)*3, "") "* [" text "](" gh_url modified_href ")"
232 | '
233 | if [ `uname -s` == "OS/390" ]; then
234 | grepcmd="pcregrep -o"
235 | echoargs=""
236 | awkscript='{
237 | level = substr($0, length($0), 1)
238 | text = substr($0, match($0, /a>.*<\/h/)+2, RLENGTH-5)
239 | href = substr($0, match($0, "href=\"([^\"]+)?\"")+6, RLENGTH-7)
240 | '"$common_awk_script"'
241 | }'
242 | else
243 | grepcmd="grep -Eo"
244 | echoargs="-e"
245 | awkscript='{
246 | level = substr($0, length($0), 1)
247 | text = substr($0, match($0, /a>.*<\/h/)+2, RLENGTH-5)
248 | href = substr($0, match($0, "href=\"[^\"]+?\"")+6, RLENGTH-7)
249 | '"$common_awk_script"'
250 | }'
251 | fi
252 | href_regex='href=\"[^\"]+?\"'
253 |
254 | # if closed is on the new line, then move it on the prev line
255 | # for example:
256 | # was: The command foo1
257 | #
258 | # became: The command foo1
259 | sed -e ':a' -e 'N' -e '$!ba' -e 's/\n<\/h/<\/h/g' |
260 |
261 | # find strings that corresponds to template
262 | $grepcmd '//g' | sed 's/<\/code>//g' |
266 |
267 | # remove g-emoji
268 | sed 's/]*[^<]*<\/g-emoji> //g' |
269 |
270 | # now all rows are like:
271 | # ... / placeholders"
300 | echo " $app_name - Create TOC for markdown from STDIN"
301 | echo " $app_name --help Show help"
302 | echo " $app_name --version Show version"
303 | return
304 | fi
305 |
306 | if [ "$1" = '--version' ]; then
307 | echo "$gh_toc_version"
308 | echo
309 | echo "os: `lsb_release -d | cut -f 2`"
310 | echo "kernel: `cat /proc/version`"
311 | echo "shell: `$SHELL --version`"
312 | echo
313 | for tool in curl wget grep awk sed; do
314 | printf "%-5s: " $tool
315 | echo `$tool --version | head -n 1`
316 | done
317 | return
318 | fi
319 |
320 | if [ "$1" = "-" ]; then
321 | if [ -z "$TMPDIR" ]; then
322 | TMPDIR="/tmp"
323 | elif [ -n "$TMPDIR" -a ! -d "$TMPDIR" ]; then
324 | mkdir -p "$TMPDIR"
325 | fi
326 | local gh_tmp_md
327 | if [ `uname -s` == "OS/390" ]; then
328 | local timestamp=$(date +%m%d%Y%H%M%S)
329 | gh_tmp_md="$TMPDIR/tmp.$timestamp"
330 | else
331 | gh_tmp_md=$(mktemp $TMPDIR/tmp.XXXXXX)
332 | fi
333 | while read input; do
334 | echo "$input" >> "$gh_tmp_md"
335 | done
336 | gh_toc_md2html "$gh_tmp_md" | gh_toc_grab ""
337 | return
338 | fi
339 |
340 | if [ "$1" = '--insert' ]; then
341 | need_replace="yes"
342 | shift
343 | fi
344 |
345 | if [ "$1" = '--no-backup' ]; then
346 | need_replace="yes"
347 | no_backup="yes"
348 | shift
349 | fi
350 |
351 | if [ "$1" = '--hide-footer' ]; then
352 | need_replace="yes"
353 | no_footer="yes"
354 | shift
355 | fi
356 |
357 | for md in "$@"
358 | do
359 | echo ""
360 | gh_toc "$md" "$#" "$need_replace" "$no_backup" "$no_footer"
361 | done
362 |
363 | echo ""
364 | echo "Created by [gh-md-toc](https://github.com/ekalinin/github-markdown-toc)"
365 | }
366 |
367 | #
368 | # Entry point
369 | #
370 | gh_toc_app "$@"
371 |
372 |
--------------------------------------------------------------------------------
/.github/workflows/e2e-test-deploy.yaml:
--------------------------------------------------------------------------------
1 | # Run secret-dependent integration tests only after /ok-to-test approval
2 | on:
3 | pull_request:
4 | repository_dispatch:
5 | types: [test-e2e-deploy-command]
6 |
7 | name: e2e tests
8 |
9 | jobs:
10 | # Repo owner has commented / on a (fork-based) pull request
11 | test-e2e-deploy:
12 | runs-on: ubuntu-latest
13 | if:
14 | github.event_name == 'repository_dispatch' &&
15 | github.event.client_payload.slash_command.command == 'test-e2e-deploy' &&
16 | github.event.client_payload.slash_command.args.named.sha != '' &&
17 | contains(github.event.client_payload.pull_request.head.sha, github.event.client_payload.slash_command.args.named.sha)
18 | steps:
19 |
20 | # Check out merge commit
21 | - name: Fork based /test-e2e-deploy checkout
22 | uses: actions/checkout@v2
23 | with:
24 | ref: 'refs/pull/${{ github.event.client_payload.pull_request.number }}/merge'
25 |
26 | #
27 |
28 | - run: aws --version
29 | - name: Configure AWS Credentials
30 | uses: aws-actions/configure-aws-credentials@v1
31 | with:
32 | aws-access-key-id: ${{ secrets.AWS_KEY_ID }}
33 | aws-secret-access-key: ${{ secrets.AWS_SEC }}
34 | aws-region: 'us-west-1'
35 | - name: Setup python 3.8
36 | uses: actions/setup-python@v2
37 | with:
38 | python-version: '3.8'
39 | - name: Install dependencies
40 | run: python -m pip install -r requirements.txt
41 | - name: Run Integration tests for deploy
42 | run: python tests/test_api.py
43 |
44 | # Update check run called "integration-fork"
45 | - uses: actions/github-script@v1
46 | id: update-check-run
47 | if: ${{ always() }}
48 | env:
49 | number: ${{ github.event.client_payload.pull_request.number }}
50 | job: ${{ github.job }}
51 | # Conveniently, job.status maps to https://developer.github.com/v3/checks/runs/#update-a-check-run
52 | conclusion: ${{ job.status }}
53 | with:
54 | github-token: ${{ secrets.GITHUB_TOKEN }}
55 | script: |
56 | const { data: pull } = await github.pulls.get({
57 | ...context.repo,
58 | pull_number: process.env.number
59 | });
60 | const ref = pull.head.sha;
61 |
62 | const { data: checks } = await github.checks.listForRef({
63 | ...context.repo,
64 | ref
65 | });
66 |
67 | const check = checks.check_runs.filter(c => c.name === process.env.job);
68 |
69 | const { data: result } = await github.checks.update({
70 | ...context.repo,
71 | check_run_id: check[0].id,
72 | status: 'completed',
73 | conclusion: process.env.conclusion
74 | });
75 |
76 | return result;
77 |
--------------------------------------------------------------------------------
/.github/workflows/help-action.yml:
--------------------------------------------------------------------------------
1 | name: help action
2 | on:
3 | repository_dispatch:
4 | types: [help-command]
5 |
6 | jobs:
7 | display-help-info:
8 | runs-on: ubuntu-latest
9 | if:
10 | github.event_name == 'repository_dispatch' &&
11 | github.event.client_payload.slash_command.command == 'help'
12 | steps:
13 | - name: Create comment
14 | uses: peter-evans/create-or-update-comment@v1
15 | with:
16 | issue-number: ${{ github.event.client_payload.github.payload.issue.number }}
17 | body: |
18 | Hello @${{ github.event.client_payload.github.actor }}!
19 | These are the list of commands available in this repo.
20 |
21 | | Command | Description |
22 | | ------- | ----------- |
23 | | /test-e2e-deploy sha=_"first 7 char of commit sha"_ | Runs the e2e test for this deployment tool. |
24 |
--------------------------------------------------------------------------------
/.github/workflows/slash-command-dispatcher.yaml:
--------------------------------------------------------------------------------
1 | # If someone with write access comments "/ok-to-test" on a pull request, emit a repository_dispatch event
2 | name: slash command dispatcher
3 |
4 | on:
5 | issue_comment:
6 | types: [created]
7 |
8 | jobs:
9 | test-e2e-deploy:
10 | runs-on: ubuntu-latest
11 | # Only run for PRs, not issue comments
12 | if: ${{ github.event.issue.pull_request }}
13 | steps:
14 | # Generate a GitHub App installation access token from an App ID and private key
15 | # To create a new GitHub App:
16 | # https://developer.github.com/apps/building-github-apps/creating-a-github-app/
17 | # See app.yml for an example app manifest
18 | - name: Generate token
19 | id: generate_token
20 | uses: tibdex/github-app-token@v1
21 | with:
22 | app_id: ${{ secrets.DEPLOY_BOT_APP_ID }}
23 | private_key: ${{ secrets.DEPLOY_BOT_PRIVATE_KEY }}
24 |
25 | - name: Slash Command Dispatch
26 | uses: peter-evans/slash-command-dispatch@v2
27 | env:
28 | TOKEN: ${{ steps.generate_token.outputs.token }}
29 | with:
30 | token: ${{ env.TOKEN }} # GitHub App installation access token
31 | #token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} # PAT or OAuth token will also work
32 | reaction-token: ${{ secrets.GITHUB_TOKEN }}
33 | issue-type: pull-request
34 | commands: test-e2e-deploy
35 | permission: write
36 |
37 | help-command:
38 | runs-on: ubuntu-latest
39 | steps:
40 | # Checkout is necessary here due to referencing a local action.
41 | # It's also necessary when using the 'config-from-file' option.
42 | # Otherwise, avoid using checkout to keep this workflow fast.
43 | - uses: actions/checkout@v2
44 |
45 | - name: Generate token
46 | id: generate_token
47 | uses: tibdex/github-app-token@v1
48 | with:
49 | app_id: ${{ secrets.DEPLOY_BOT_APP_ID }}
50 | private_key: ${{ secrets.DEPLOY_BOT_PRIVATE_KEY }}
51 |
52 | - name: slash command dispatch
53 | uses: peter-evans/slash-command-dispatch@v2
54 | env:
55 | TOKEN: ${{ steps.generate_token.outputs.token }}
56 | with:
57 | token: ${{ env.TOKEN }}
58 | commands: help
59 | permission: none
60 | issue-type: both
61 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | pip-wheel-metadata/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | target/
76 |
77 | # Jupyter Notebook
78 | .ipynb_checkpoints
79 |
80 | # IPython
81 | profile_default/
82 | ipython_config.py
83 |
84 | # pyenv
85 | .python-version
86 |
87 | # pipenv
88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
91 | # install all needed dependencies.
92 | #Pipfile.lock
93 |
94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95 | __pypackages__/
96 |
97 | # Celery stuff
98 | celerybeat-schedule
99 | celerybeat.pid
100 |
101 | # SageMath parsed files
102 | *.sage.py
103 |
104 | # Environments
105 | .env
106 | .venv
107 | env/
108 | venv/
109 | ENV/
110 | env.bak/
111 | venv.bak/
112 |
113 | # Spyder project settings
114 | .spyderproject
115 | .spyproject
116 |
117 | # Rope project settings
118 | .ropeproject
119 |
120 | # mkdocs documentation
121 | /site
122 |
123 | # mypy
124 | .mypy_cache/
125 | .dmypy.json
126 | dmypy.json
127 |
128 | # Pyre type checker
129 | .pyre/
130 |
131 | # Editor
132 | .idea/
--------------------------------------------------------------------------------
/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 | ## ⚠️ BentoCTL project has been deprecated
2 |
3 | Plese see the latest BentoML documentation on OCI-container based deployment workflow: https://docs.bentoml.com/
4 |
5 | ## Bentoctl AWS Lambda deployment
6 |
7 | Bentoctl is a CLI tool for deploying your machine-learning models to any cloud platforms and serving predictions via REST APIs.
8 | It built on top of [BentoML: the unified model serving framework](https://github.com/bentoml/bentoml), and makes it easy to bring any BentoML packaged model to production.
9 |
10 | This repo contains the Bentoctl AWS Lambda deployment operator. This operator defines the terraform configuration for deploying the Lambda function and how to build docker image that's compatible with AWS Lambda.
11 |
12 |
13 | > **Note:** This operator is compatible with BentoML version 1.0.0 and above. For older versions, please switch to the branch `pre-v1.0` and follow the instructions in the README.md.
14 |
15 |
16 | ## Table of Contents
17 |
18 | * [Prerequisites](#prerequisites)
19 | * [Quickstart with bentoctl](#quickstart-with-bentoctl)
20 | * [Configuration options](#configuration-options)
21 |
22 |
23 |
24 |
25 |
26 | ## Prerequisites
27 |
28 | 1. Bentoml - BentoML version 1.0 and greater. Please follow the [Installation guide](https://docs.bentoml.org/en/latest/quickstart.html#installation).
29 | 2. Terraform - [Terraform](https://www.terraform.io/) is a tool for building, configuring, and managing infrastructure.
30 | 3. AWS CLI installed and configured with an AWS account with permission to the Cloudformation, Lamba, API Gateway and ECR. Please follow the [Installation guide](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html).
31 | 4. Docker - Install instruction: https://docs.docker.com/installll instruction: https://www.terraform.io/downloads.html
32 | 5. A built Bento project. For this guide, we will use the Iris classifier bento from the [BentoML quickstart guide](https://docs.bentoml.org/en/latest/quickstart.html#quickstart).
33 |
34 | ## Quickstart with bentoctl
35 |
36 | Bentoctl is a CLI tool that you can use to deploy bentos to Lambda. It helps in configuring and managing your deployments super easy.
37 |
38 | 1. Install bentoctl via pip
39 | ```
40 | $ pip install bentoctl
41 | ```
42 |
43 | 2. Install AWS Lambda operator
44 |
45 | Bentoctl will install the official AWS Lambda operator and its dependencies.
46 |
47 | ```
48 | $ bentoctl operator install aws-lambda
49 | ```
50 |
51 | 3. Initialize deployment with bentoctl
52 |
53 | Follow the interactive guide to initialize deployment project.
54 |
55 | ```bash
56 | $ bentoctl init
57 |
58 | Bentoctl Interactive Deployment Config Builder
59 |
60 | Welcome! You are now in interactive mode.
61 |
62 | This mode will help you setup the deployment_config.yaml file required for
63 | deployment. Fill out the appropriate values for the fields.
64 |
65 | (deployment config will be saved to: ./deployment_config.yaml)
66 |
67 | api_version: v1
68 | name: demo
69 | operator: aws-lambda
70 | template: terraform
71 | spec:
72 | region: us-west-1
73 | timeout: 10
74 | memory_size: 512
75 | filename for deployment_config [deployment_config.yaml]:
76 | deployment config file exists! Should I override? [Y/n]:
77 | deployment config generated to: deployment_config.yaml
78 | ✨ generated template files.
79 | - bentoctl.tfvars
80 | - main.tf
81 | ```
82 |
83 | 4. Build and push AWS Lambda comptable docker image to registry
84 |
85 | Bentoctl will build and push the Lambda compatible docker image to the AWS ECR repository.
86 |
87 | ```bash
88 | bentoctl build -b iris_classifier:latest -f deployment_config.yaml
89 |
90 | Step 1/20 : FROM bentoml/bento-server:1.0.0a7-python3.7-debian-runtime
91 | ---> dde7b88477b1
92 | Step 2/20 : ARG UID=1034
93 | ---> Running in b8f4ae1d8b08
94 | ---> e6c313c8d9ea
95 | Step 3/20 : ARG GID=1034
96 | ....
97 | Step 20/20 : ENTRYPOINT [ "/opt/conda/bin/python", "-m", "awslambdaric" ]
98 | ---> Running in 4e56057f3b18
99 | ---> dca82bca9034
100 | Successfully built dca82bca9034
101 | Successfully tagged aws-lambda-iris_classifier:btzv5wfv665trhcu
102 | 🔨 Image build!
103 | The push refers to repository [192023623294.dkr.ecr.us-west-1.amazonaws.com/quickstart]
104 | btzv5wfv665trhcu: digest: sha256:ffcd120f7629122cf5cd95664e4fd28e9a50e799be7bb23f0b5b03f14ca5c672 size: 3253
105 | 32096534b881: Pushed
106 | f709d8f0f57d: Pushed
107 | 7d30486f5c78: Pushed
108 | ...
109 | c1065d45b872: Pushed
110 | 🚀 Image pushed!
111 | ✨ generated template files.
112 | - bentoctl.tfvars
113 | The push refers to repository [192023623294.dkr.ecr.us-west-1.amazonaws.com/quickstart]
114 | ```
115 |
116 | 5. Apply Deployment with Terraform
117 |
118 | 1. Initialize terraform project
119 | ```bash
120 | terraform init
121 | ```
122 |
123 | 2. Apply terraform project to create Lambda deployment
124 |
125 | ```bash
126 | terraform apply -var-file=bentoctl.tfvars -auto-approve
127 |
128 | aws_iam_role.lambda_exec: Creating...
129 | aws_apigatewayv2_api.lambda: Creating...
130 | aws_apigatewayv2_api.lambda: Creation complete after 1s [id=ka8h2p2yfh]
131 | aws_cloudwatch_log_group.api_gw: Creating...
132 | aws_cloudwatch_log_group.api_gw: Creation complete after 0s [id=/aws/api_gw/quickstart-gw]
133 | aws_apigatewayv2_stage.lambda: Creating...
134 | aws_iam_role.lambda_exec: Creation complete after 3s [id=quickstart-iam]
135 | aws_iam_role_policy_attachment.lambda_policy: Creating...
136 | aws_lambda_function.fn: Creating...
137 | aws_apigatewayv2_stage.lambda: Creation complete after 2s [id=$default]
138 | aws_iam_role_policy_attachment.lambda_policy: Creation complete after 1s [id=quickstart-iam-20220414203448384500000001]
139 | aws_lambda_function.fn: Still creating... [10s elapsed]
140 | aws_lambda_function.fn: Still creating... [20s elapsed]
141 | aws_lambda_function.fn: Still creating... [30s elapsed]
142 | aws_lambda_function.fn: Still creating... [40s elapsed]
143 | aws_lambda_function.fn: Creation complete after 41s [id=quickstart-function]
144 | aws_lambda_permission.api_gw: Creating...
145 | aws_cloudwatch_log_group.lg: Creating...
146 | aws_apigatewayv2_integration.lambda: Creating...
147 | aws_lambda_permission.api_gw: Creation complete after 0s [id=AllowExecutionFromAPIGateway]
148 | aws_cloudwatch_log_group.lg: Creation complete after 0s [id=/aws/lambda/quickstart-function]
149 | aws_apigatewayv2_integration.lambda: Creation complete after 1s [id=8gumjws]
150 | aws_apigatewayv2_route.root: Creating...
151 | aws_apigatewayv2_route.services: Creating...
152 | aws_apigatewayv2_route.root: Creation complete after 0s [id=jjp5f23]
153 | aws_apigatewayv2_route.services: Creation complete after 0s [id=8n57a1d]
154 |
155 | Apply complete! Resources: 11 added, 0 changed, 0 destroyed.
156 |
157 | Outputs:
158 |
159 | base_url = "https://ka8h2p2yfh.execute-api.us-west-1.amazonaws.com/"
160 | function_name = "quickstart-function"
161 | image_tag = "192023623294.dkr.ecr.us-west-1.amazonaws.com/quickstart:btzv5wfv665trhcu"
162 | ```
163 |
164 | 6. Test deployed endpoint
165 |
166 | The `iris_classifier` uses the `/classify` endpoint for receiving requests so the full URL for the classifier will be in the form `{EndpointUrl}/classify`
167 |
168 | ```bash
169 | URL=$(terraform output -json | jq -r .base_url.value)classify
170 | curl -i \
171 | --header "Content-Type: application/json" \
172 | --request POST \
173 | --data '[5.1, 3.5, 1.4, 0.2]' \
174 | $URL
175 |
176 | HTTP/2 200
177 | date: Thu, 14 Apr 2022 23:02:45 GMT
178 | content-type: application/json
179 | content-length: 1
180 | apigw-requestid: Ql8zbicdSK4EM5g=
181 |
182 | 0%
183 | ```
184 |
185 | 7. Delete deployment
186 | Use the `bentoctl destroy` command to remove the registry and the deployment
187 |
188 | ```bash
189 | bentoctl destroy -f deployment_config.yaml
190 | ```
191 | ## Configuration options
192 |
193 | * `region`: AWS region for Lambda deployment
194 | * `timeout`: Timeout per request
195 | * `memory_size`: The memory for your function, set a value between 128 MB and 10,240 MB in 1-MB increments
196 |
--------------------------------------------------------------------------------
/bentoctl_lambda/__init__.py:
--------------------------------------------------------------------------------
1 | from .create_deployable import create_deployable
2 | from .generate import generate
3 | from .registry_utils import create_repository, delete_repository
4 |
5 | __all__ = ["create_deployable", "generate", "create_repository", "delete_repository"]
6 |
--------------------------------------------------------------------------------
/bentoctl_lambda/aws_lambda/README.md:
--------------------------------------------------------------------------------
1 | AWS Lambda supports deployment via containers of upto 10gb and that is what we
2 | are using for deploying bentos into lambda. The docs can be accessed
3 | [here](https://docs.aws.amazon.com/lambda/latest/dg/images-create.html).
4 |
5 | In order for AWS to run the container in lambda they require the following
6 | - Defining a handler: this is the python code that will handle the `event` and
7 | `context` dict that is passed when ever the lambda function is invoked. Our
8 | handler is defined in ./app.py and it uses
9 | [Mangum](https://github.com/jordaneremieff/mangum) to handle the incoming
10 | requrest.
11 | - setup aws-lambda-ric: this is the runtime-interface-client that AWS has
12 | developed to interface with the Runtime API
13 | (https://github.com/aws/aws-lambda-python-runtime-interface-client). The
14 | ./entry_script.sh handles invoking and loading the handler with the RIC.
15 |
16 | ## Terraform setup
17 |
18 | AWS lambda is setup with an HTTP API infront to act as public endpoint for the
19 | lambda function. Any request that comes is passed onto the lambda function. The
20 | request is handled by mangum which transforms it into a ASGI request that can be
21 | processed by bentoml's ASGI app.
22 |
23 | All the logs are in cloudwatch and IAM policies for every infra is created by
24 | the terraform template.
25 |
26 | ## Dev setup
27 |
28 | Right now running it locally is not possible. There is a lambda Runtime Emulator
29 | that loads any handler and emulates the AWS interface but it is still work in
30 | progress.
31 |
--------------------------------------------------------------------------------
/bentoctl_lambda/aws_lambda/app.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from bentoml import load
4 | from mangum import Mangum
5 |
6 | API_GATEWAY_STAGE = os.environ.get("API_GATEWAY_STAGE", None)
7 |
8 | print("Loading from dir...")
9 | bento_service = load("./", standalone_load=True)
10 |
11 | print("bento service", bento_service)
12 |
13 | mangum_app = Mangum(bento_service.asgi_app, api_gateway_base_path=API_GATEWAY_STAGE)
14 |
--------------------------------------------------------------------------------
/bentoctl_lambda/aws_lambda/aws-lambda-rie-x86:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bentoml/aws-lambda-deploy/80ebada800e6db5af66bf38d077ebd59c004f89e/bentoctl_lambda/aws_lambda/aws-lambda-rie-x86
--------------------------------------------------------------------------------
/bentoctl_lambda/aws_lambda/bentoml_server_config.yaml:
--------------------------------------------------------------------------------
1 | api_server:
2 | metrics:
3 | enabled: False
4 |
--------------------------------------------------------------------------------
/bentoctl_lambda/aws_lambda/entry_script.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | if [ -z "${AWS_LAMBDA_RUNTIME_API}" ]; then
3 | exec /usr/local/bin/aws-lambda-rie /usr/local/bin/python -m awslambdaric $@
4 | else
5 | exec /usr/local/bin/python -m awslambdaric $@
6 | fi
7 |
--------------------------------------------------------------------------------
/bentoctl_lambda/aws_lambda/template.j2:
--------------------------------------------------------------------------------
1 | {% extends bento_base_template %}
2 | {% set bento__home = "/tmp" %}
3 | {% block SETUP_BENTO_ENTRYPOINT %}
4 | EXPOSE 3000
5 | ENV BENTOML_CONFIG={{bento__path}}/bentoml_config.yaml
6 |
7 | RUN {{ common.mount_cache("/root/.cache/pip") }} pip install awslambdaric==2.0.8 mangum==0.12.3
8 |
9 | USER root
10 | ADD ./aws-lambda-rie /usr/local/bin/aws-lambda-rie
11 | RUN chmod +x /usr/local/bin/aws-lambda-rie
12 | RUN chmod +x {{bento__path}}/env/docker/entry_script.sh
13 | ENTRYPOINT ["{{bento__path}}/env/docker/entry_script.sh"]
14 | {% endblock %}
15 |
--------------------------------------------------------------------------------
/bentoctl_lambda/create_deployable.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import os
4 | import shutil
5 | from pathlib import Path
6 | from sys import version_info
7 | from typing import Any
8 |
9 | from attr import asdict
10 |
11 | if version_info >= (3, 8):
12 | from shutil import copytree
13 | else:
14 | from backports.shutil_copytree import copytree
15 |
16 | from bentoml._internal.bento.bento import BentoInfo
17 | from bentoml._internal.bento.build_config import DockerOptions
18 | from bentoml._internal.bento.gen import generate_dockerfile
19 |
20 | LAMBDA_DIR = Path(os.path.dirname(__file__), "aws_lambda")
21 | TEMPLATE_PATH = LAMBDA_DIR.joinpath("template.j2")
22 | APP_PATH = LAMBDA_DIR.joinpath("app.py")
23 | ENTRY_SCRIPT = LAMBDA_DIR.joinpath("entry_script.sh")
24 | AWS_LAMBDA_RIE = LAMBDA_DIR.joinpath("aws-lambda-rie-x86")
25 | BENTOML_CONFIG_FILE = LAMBDA_DIR.joinpath("bentoml_server_config.yaml")
26 |
27 |
28 | def create_deployable(
29 | bento_path: str,
30 | destination_dir: str,
31 | bento_metadata: dict[str, Any],
32 | overwrite_deployable: bool,
33 | ) -> str:
34 | """
35 | The deployable is the bento along with all the modifications (if any)
36 | requried to deploy to the cloud service.
37 |
38 | Parameters
39 | ----------
40 | bento_path: str
41 | Path to the bento from the bento store.
42 | destination_dir: str
43 | directory to create the deployable into.
44 | bento_metadata: dict
45 | metadata about the bento.
46 |
47 | Returns
48 | -------
49 | docker_context_path : str
50 | path to the docker context.
51 | """
52 |
53 | deployable_path = Path(destination_dir)
54 | copytree(bento_path, deployable_path, dirs_exist_ok=True)
55 |
56 | bento_metafile = Path(bento_path, "bento.yaml")
57 | with bento_metafile.open("r", encoding="utf-8") as metafile:
58 | info = BentoInfo.from_yaml_file(metafile)
59 |
60 | options = asdict(info.docker)
61 | options["dockerfile_template"] = TEMPLATE_PATH
62 |
63 | dockerfile_path = deployable_path.joinpath("env", "docker", "Dockerfile")
64 | with dockerfile_path.open("w", encoding="utf-8") as dockerfile:
65 | dockerfile_generated = generate_dockerfile(
66 | DockerOptions(**options).with_defaults(),
67 | str(deployable_path),
68 | use_conda=not info.conda.is_empty(),
69 | )
70 | dockerfile.write(dockerfile_generated)
71 |
72 | # copy over app.py file
73 | shutil.copy(str(APP_PATH), os.path.join(deployable_path, "app.py"))
74 |
75 | # the entry_script.sh file that will be the entrypoint
76 | shutil.copy(
77 | str(ENTRY_SCRIPT),
78 | os.path.join(deployable_path, "env", "docker", "entry_script.sh"),
79 | )
80 |
81 | # aws-lambda runtime-interface-emulator - check docs for more info
82 | shutil.copy(
83 | str(AWS_LAMBDA_RIE),
84 | os.path.join(deployable_path, "aws-lambda-rie"),
85 | )
86 |
87 | # bentoml_config_file to dissable /metrics
88 | shutil.copy(
89 | str(BENTOML_CONFIG_FILE),
90 | os.path.join(deployable_path, "bentoml_config.yaml"),
91 | )
92 |
93 | return str(deployable_path)
94 |
--------------------------------------------------------------------------------
/bentoctl_lambda/generate.py:
--------------------------------------------------------------------------------
1 | import os
2 | import shutil
3 |
4 | from bentoctl.exceptions import TemplateExists, TemplateTypeNotDefined
5 |
6 | from bentoctl_lambda.parameters import DeploymentValues
7 | from bentoctl_lambda.utils import get_metadata
8 |
9 |
10 | def generate(name, spec, template_type, destination_dir, values_only=True):
11 | """
12 | generates the template corresponding to the template_type.
13 |
14 | Parameters
15 | ----------
16 | name : str
17 | deployment name to be used by the template. This name will be used
18 | to create the resource names.
19 | spec : dict
20 | The properties of the deployment (specifications) passed from the
21 | deployment_config's `spec` section.
22 | template_type: str
23 | The type of template that is to be generated by the operator. The
24 | available ones are [terraform, cloudformation]
25 |
26 | Returns
27 | -------
28 | generated_path : str
29 | The path for the generated template.
30 | """
31 | if template_type == "terraform":
32 | template_file_name = "terraform_default.tf"
33 | generated_template_file_name = "main.tf"
34 | generated_params_file_name = "bentoctl.tfvars"
35 | else:
36 | raise TemplateTypeNotDefined(template_type)
37 |
38 | generated_files = [generated_params_file_name]
39 | generated_template_file_path = os.path.join(
40 | destination_dir, generated_template_file_name
41 | )
42 | if not values_only:
43 | if os.path.exists(generated_template_file_path):
44 | raise TemplateExists(generated_template_file_path)
45 | shutil.copyfile(
46 | os.path.join(os.path.dirname(__file__), f"templates/{template_file_name}"),
47 | generated_template_file_path,
48 | )
49 | generated_files.append(generated_template_file_name)
50 |
51 | # generate params file
52 | params = DeploymentValues(name, spec, template_type)
53 | params.to_params_file(os.path.join(destination_dir, generated_params_file_name))
54 |
55 | return generated_files
56 |
--------------------------------------------------------------------------------
/bentoctl_lambda/parameters.py:
--------------------------------------------------------------------------------
1 | from collections import UserDict
2 |
3 | DEPLOYMENT_PARAMS_WARNING = """# This file is maintained automatically by
4 | # "bentoctl generate" and "bentoctl build" commands.
5 | # Manual edits may be lost the next time these commands are run.
6 |
7 | """
8 |
9 |
10 | def parse_image_tag(image_tag: str):
11 | registry_url, tag = image_tag.split("/")
12 | repository, version = tag.split(":")
13 |
14 | return registry_url, repository, version
15 |
16 |
17 | class DeploymentValues(UserDict):
18 | def __init__(self, name, spec, template_type):
19 | if "image_tag" in spec:
20 | _, image_repository, image_version = parse_image_tag(spec["image_tag"])
21 | spec["image_repository"] = image_repository
22 | spec["image_version"] = image_version
23 |
24 | super().__init__({"deployment_name": name, **spec})
25 | self.template_type = template_type
26 |
27 | def to_params_file(self, file_path):
28 | if self.template_type == "terraform":
29 | self.generate_terraform_tfvars_file(file_path)
30 |
31 | @classmethod
32 | def from_params_file(cls, file_path):
33 | pass
34 |
35 | def generate_terraform_tfvars_file(self, file_path):
36 | params = []
37 | for param_name, param_value in self.items():
38 | params.append(f'{param_name} = "{param_value}"')
39 |
40 | with open(file_path, "w") as params_file:
41 | params_file.write(DEPLOYMENT_PARAMS_WARNING)
42 | params_file.write("\n".join(params))
43 | params_file.write("\n")
44 |
--------------------------------------------------------------------------------
/bentoctl_lambda/registry_utils.py:
--------------------------------------------------------------------------------
1 | import base64
2 |
3 | import boto3
4 |
5 |
6 | def get_ecr_login_info(region, repository_id):
7 | ecr_client = boto3.client("ecr", region)
8 | token = ecr_client.get_authorization_token(registryIds=[repository_id])
9 | username, password = (
10 | base64.b64decode(token["authorizationData"][0]["authorizationToken"])
11 | .decode("utf-8")
12 | .split(":")
13 | )
14 | registry_url = token["authorizationData"][0]["proxyEndpoint"]
15 |
16 | return registry_url, username, password
17 |
18 |
19 | def get_repository(ecr_client, repository_name):
20 | result = ecr_client.describe_repositories(repositoryNames=[repository_name])
21 | repository_id = result["repositories"][0]["registryId"]
22 | repository_uri = result["repositories"][0]["repositoryUri"]
23 |
24 | return repository_id, repository_uri
25 |
26 |
27 | def create_ecr_repository_if_not_exists(region, repository_name):
28 | ecr_client = boto3.client("ecr", region)
29 | try:
30 | repository_id, repository_uri = get_repository(ecr_client, repository_name)
31 | except ecr_client.exceptions.RepositoryNotFoundException:
32 | result = ecr_client.create_repository(repositoryName=repository_name)
33 | repository_id = result["repository"]["registryId"]
34 | repository_uri = result["repository"]["repositoryUri"]
35 | return repository_id, repository_uri
36 |
37 |
38 | def create_repository(repository_name, operator_spec):
39 | """
40 | Create the ECR registry and return the info.
41 | """
42 | repo_id, _ = create_ecr_repository_if_not_exists(
43 | operator_spec.get("region"), repository_name
44 | )
45 | registry_url, username, password = get_ecr_login_info(
46 | operator_spec["region"], repo_id
47 | )
48 | repository_url = f"{registry_url}/{repository_name}"
49 |
50 | return repository_url, username, password
51 |
52 |
53 | def delete_repository(repository_name, operator_spec):
54 | """
55 | Destroy the ECR registry created.
56 | """
57 | ecr_client = boto3.client("ecr", operator_spec.get("region"))
58 | try:
59 | get_repository(ecr_client, repository_name)
60 | ecr_client.delete_repository(repositoryName=repository_name, force=True)
61 | except ecr_client.exceptions.RepositoryNotFoundException:
62 | print(f"Repository {repository_name} not found. Skipping registry cleanup")
63 |
--------------------------------------------------------------------------------
/bentoctl_lambda/templates/cloudformation_default.yaml:
--------------------------------------------------------------------------------
1 | AWSTemplateFormatVersion: '2010-09-09'
2 | Parameters:
3 | deploymentName:
4 | Type: String
5 | repositoryName:
6 | Type: String
7 | repositoryTag:
8 | Type: String
9 | memorySize:
10 | Type: Number
11 | Default: 514
12 | timeout:
13 | Type: Number
14 | Default: 20
15 | Globals:
16 | Api:
17 | Auth:
18 | AddDefaultAuthorizerToCorsPreflight: false
19 | ApiKeyRequired: false
20 | DefaultAuthorizer: NONE
21 | BinaryMediaTypes:
22 | - '*~1*'
23 | Cors: '''*'''
24 | Function:
25 | MemorySize: !Ref memorySize
26 | Timeout: !Ref timeout
27 | Resources:
28 | pandas:
29 | Properties:
30 | Environment:
31 | Variables:
32 | API_GATEWAY_STAGE: ''
33 | Events:
34 | Api:
35 | Properties:
36 | Method: any
37 | Path: /{proxy+}
38 | Type: HttpApi
39 | FunctionName: !Sub ${deploymentName}-fn
40 | ImageConfig:
41 | Command:
42 | - app.mangum_app
43 | ImageUri: !Sub '${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${repositoryName}:${repositoryTag}'
44 | PackageType: Image
45 | Type: AWS::Serverless::Function
46 | Transform: AWS::Serverless-2016-10-31
47 | Outputs:
48 | EndpointUrl:
49 | Value: !Sub "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com"
50 | Description: URL for endpoint
51 |
--------------------------------------------------------------------------------
/bentoctl_lambda/templates/terraform_default.tf:
--------------------------------------------------------------------------------
1 | terraform {
2 | required_providers {
3 | aws = {
4 | source = "hashicorp/aws"
5 | version = "~> 4.0.0"
6 | }
7 | }
8 |
9 | required_version = "~> 1.0"
10 | }
11 |
12 | provider "aws" {
13 | region = var.region
14 | }
15 |
16 | ################################################################################
17 | # Input variable definitions
18 | ################################################################################
19 |
20 | variable "deployment_name" {
21 | type = string
22 | }
23 |
24 | variable "image_tag" {
25 | type = string
26 | }
27 |
28 | variable "image_repository" {
29 | type = string
30 | }
31 |
32 | variable "image_version" {
33 | type = string
34 | }
35 |
36 | variable "region" {
37 | description = "AWS region for all resources."
38 |
39 | type = string
40 | default = "ap-south-1"
41 | }
42 |
43 | variable "timeout" {
44 | description = "Timout for the Lambda Function."
45 | type = number
46 | default = 300
47 | }
48 |
49 | variable "memory_size" {
50 | description = "Memory allocated to lambda function."
51 | type = number
52 | default = 256
53 | }
54 |
55 | ################################################################################
56 | # Resource definitions
57 | ################################################################################
58 |
59 | data "aws_ecr_repository" "service" {
60 | name = var.image_repository
61 | }
62 |
63 | data "aws_ecr_image" "service_image" {
64 | repository_name = data.aws_ecr_repository.service.name
65 | image_tag = var.image_version
66 | }
67 |
68 | resource "aws_lambda_function" "fn" {
69 | function_name = "${var.deployment_name}-function"
70 | role = aws_iam_role.lambda_exec.arn
71 |
72 | timeout = var.timeout
73 | memory_size = var.memory_size
74 | image_uri = "${data.aws_ecr_repository.service.repository_url}@${data.aws_ecr_image.service_image.id}"
75 | package_type = "Image"
76 |
77 | image_config {
78 | command = [
79 | "app.mangum_app",
80 | ]
81 | entry_point = []
82 | }
83 | }
84 |
85 | resource "aws_cloudwatch_log_group" "lg" {
86 | name = "/aws/lambda/${aws_lambda_function.fn.function_name}"
87 |
88 | retention_in_days = 30
89 | }
90 |
91 | resource "aws_iam_role" "lambda_exec" {
92 | name = "${var.deployment_name}-iam"
93 |
94 | assume_role_policy = jsonencode({
95 | Version = "2012-10-17"
96 | Statement = [{
97 | Action = "sts:AssumeRole"
98 | Effect = "Allow"
99 | Sid = ""
100 | Principal = {
101 | Service = "lambda.amazonaws.com"
102 | }
103 | }
104 | ]
105 | })
106 | }
107 |
108 | resource "aws_iam_role_policy_attachment" "lambda_policy" {
109 | role = aws_iam_role.lambda_exec.name
110 | policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
111 | }
112 |
113 | resource "aws_apigatewayv2_api" "lambda" {
114 | name = "${var.deployment_name}-gw"
115 | protocol_type = "HTTP"
116 | }
117 |
118 | resource "aws_apigatewayv2_stage" "lambda" {
119 | api_id = aws_apigatewayv2_api.lambda.id
120 |
121 | name = "$default"
122 | auto_deploy = true
123 |
124 | access_log_settings {
125 | destination_arn = aws_cloudwatch_log_group.api_gw.arn
126 |
127 | format = jsonencode({
128 | requestId = "$context.requestId"
129 | sourceIp = "$context.identity.sourceIp"
130 | requestTime = "$context.requestTime"
131 | protocol = "$context.protocol"
132 | httpMethod = "$context.httpMethod"
133 | resourcePath = "$context.resourcePath"
134 | routeKey = "$context.routeKey"
135 | status = "$context.status"
136 | responseLength = "$context.responseLength"
137 | integrationErrorMessage = "$context.integrationErrorMessage"
138 | }
139 | )
140 | }
141 | }
142 |
143 | resource "aws_apigatewayv2_integration" "lambda" {
144 | api_id = aws_apigatewayv2_api.lambda.id
145 |
146 | integration_uri = aws_lambda_function.fn.invoke_arn
147 | integration_type = "AWS_PROXY"
148 | integration_method = "POST"
149 | }
150 |
151 | resource "aws_apigatewayv2_route" "root" {
152 | api_id = aws_apigatewayv2_api.lambda.id
153 |
154 | route_key = "ANY /"
155 | target = "integrations/${aws_apigatewayv2_integration.lambda.id}"
156 | }
157 |
158 | resource "aws_apigatewayv2_route" "services" {
159 | api_id = aws_apigatewayv2_api.lambda.id
160 |
161 | route_key = "ANY /{proxy+}"
162 | target = "integrations/${aws_apigatewayv2_integration.lambda.id}"
163 | }
164 |
165 | resource "aws_cloudwatch_log_group" "api_gw" {
166 | name = "/aws/api_gw/${aws_apigatewayv2_api.lambda.name}"
167 | retention_in_days = 30
168 | }
169 |
170 | resource "aws_lambda_permission" "api_gw" {
171 | statement_id = "AllowExecutionFromAPIGateway"
172 | action = "lambda:InvokeFunction"
173 | function_name = aws_lambda_function.fn.function_name
174 | principal = "apigateway.amazonaws.com"
175 | source_arn = "${aws_apigatewayv2_api.lambda.execution_arn}/*/*"
176 | }
177 |
178 | ################################################################################
179 | # Output value definitions
180 | ################################################################################
181 |
182 | output "function_name" {
183 | description = "Name of the Lambda function."
184 | value = aws_lambda_function.fn.function_name
185 | }
186 |
187 | output "image_tag" {
188 | description = "The Image tag that is used for creating the function"
189 | value = var.image_tag
190 | }
191 | output "endpoint" {
192 | description = "Base URL for API Gateway stage."
193 | value = aws_apigatewayv2_stage.lambda.invoke_url
194 | }
195 |
--------------------------------------------------------------------------------
/bentoctl_lambda/utils.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import boto3
4 | import fs
5 | from bentoml.bentos import Bento
6 |
7 |
8 | def get_metadata(path: str):
9 | metadata = {}
10 |
11 | bento = Bento.from_fs(fs.open_fs(path))
12 | metadata["tag"] = bento.tag
13 | metadata["bentoml_version"] = ".".join(bento.info.bentoml_version.split(".")[:3])
14 |
15 | python_version_txt_path = "env/python/version.txt"
16 | python_version_txt_path = os.path.join(path, python_version_txt_path)
17 | with open(python_version_txt_path, "r") as f:
18 | python_version = f.read()
19 | metadata["python_version"] = ".".join(python_version.split(".")[:2])
20 |
21 | return metadata
22 |
--------------------------------------------------------------------------------
/operator_config.py:
--------------------------------------------------------------------------------
1 | OPERATOR_SCHEMA = {
2 | "region": {
3 | "required": True,
4 | "type": "string",
5 | "default": "us-west-1",
6 | "help_message": "AWS region for Lambda deployment",
7 | },
8 | "timeout": {
9 | "required": False,
10 | "type": "integer",
11 | "coerce": int,
12 | "default": 10,
13 | "help_message": "Timeout per request",
14 | },
15 | "memory_size": {
16 | "required": False,
17 | "type": "integer",
18 | "coerce": int,
19 | "default": 512,
20 | "help_message": "The memory for your function, set a value between 128 MB and 10,240 MB in 1-MB increments",
21 | },
22 | }
23 |
24 | OPERATOR_NAME = "aws-lambda"
25 |
26 | OPERATOR_MODULE = "bentoctl_lambda"
27 |
28 | OPERATOR_DEFAULT_TEMPLATE = "terraform"
29 |
30 | OPERATOR_AVAILABLE_TEMPLATES = ["terraform"]
31 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | boto3
2 | backports.shutil_copytree
--------------------------------------------------------------------------------
/tests/classifier.py:
--------------------------------------------------------------------------------
1 | # iris_classifier.py
2 | from bentoml import BentoService, api, env
3 | from bentoml.adapters import DataframeInput, FileInput, JsonInput
4 | from bentoml.types import FileLike, JsonSerializable
5 |
6 |
7 | @env(infer_pip_packages=True)
8 | class TestService(BentoService):
9 | @api(input=DataframeInput(), batch=True)
10 | def dfapi(self, df):
11 | print(df)
12 | return df
13 |
14 | @api(input=JsonInput(), batch=False)
15 | def jsonapi(self, json: JsonSerializable):
16 | print(json)
17 | return json
18 |
19 | @api(input=FileInput(), batch=False)
20 | def fileapi(self, file_stream: FileLike):
21 | print(file_stream)
22 | if file_stream.bytes_ is not None:
23 | return file_stream.bytes_
24 | else:
25 | return file_stream._stream.read()
26 |
--------------------------------------------------------------------------------
/tests/test_api.py:
--------------------------------------------------------------------------------
1 | import os
2 | import shutil
3 | import sys
4 | import tempfile
5 | import time
6 |
7 | import requests
8 | from classifier import TestService
9 | from pandas import DataFrame
10 |
11 | sys.path.append("./")
12 | from delete import delete
13 | from deploy import deploy
14 | from describe import describe
15 |
16 |
17 | class Setup:
18 | def __init__(self):
19 | """
20 | Setup the deployment on the deployment choosen
21 | """
22 | self.deployment_name = "lambda_bento_deploy_test"
23 | self.dirpath = tempfile.mkdtemp()
24 | print("temp dir {} created!".format(self.dirpath))
25 | self.saved_dir = os.path.join(self.dirpath, "saved_dir")
26 |
27 | # make config file
28 | config = """
29 | {
30 | "region": "us-west-1",
31 | "timeout": 60,
32 | "memory_size": 1024
33 | }
34 | """
35 | self.config_file = os.path.join(self.dirpath, "config.json")
36 | with open(self.config_file, "w") as f:
37 | f.write(config)
38 |
39 | # make bento service
40 | os.mkdir(self.saved_dir)
41 | test_service = TestService()
42 | # test_service.pack()
43 | test_service.save_to_dir(self.saved_dir)
44 |
45 | def make_deployment(self):
46 | deploy(self.saved_dir, self.deployment_name, self.config_file)
47 | info_json = describe(self.deployment_name, self.config_file)
48 | url = info_json["EndpointUrl"] + "/{}"
49 |
50 | # ping /healthz to check if deployment is up
51 | # attempt = 0
52 | # while attempt < 5:
53 | # time.sleep(10)
54 | # if urllib.request.urlopen(url.format("healthz")).status == 200:
55 | # break
56 | time.sleep(20)
57 | return url
58 |
59 | def teardown(self):
60 | delete(self.deployment_name, self.config_file)
61 | shutil.rmtree(self.dirpath)
62 | print("Removed {}!".format(self.dirpath))
63 |
64 |
65 | def test_json(url):
66 | """
67 | GIVEN the api is deployed
68 | WHEN a valid json is given
69 | THEN accepts the binary_data and returns it
70 | """
71 | headers = {"content-type": "application/json"}
72 | input_json = "[[1, 2, 3, 4]]"
73 | resp = requests.post(url, data=input_json, headers=headers)
74 | assert resp.ok
75 | assert resp.content == bytearray(input_json, "ascii")
76 |
77 |
78 | def test_df(url):
79 | """
80 | GIVEN the api is deployed
81 | WHEN a dataframe is passed, as json or csv
82 | THEN accepts the binary_data and returns it
83 | """
84 | input_array = [[1, 2, 3, 4]]
85 |
86 | # request as json
87 | resp = requests.post(url, json=input_array)
88 | assert resp.ok
89 | assert DataFrame(resp.json()).to_json() == DataFrame(input_array).to_json()
90 |
91 | # request as csv
92 | headers = {"content-type": "text/csv"}
93 | csv = DataFrame(input_array).to_csv(index=False)
94 | resp = requests.post(url.format("dfapi"), data=csv, headers=headers)
95 | assert resp.ok
96 | assert DataFrame(resp.json()).to_json() == DataFrame(input_array).to_json()
97 |
98 |
99 | def test_files(url):
100 | """
101 | GIVEN the api is deployed
102 | WHEN a file is passed either as raw bytes with any content-type or as mulitpart/form
103 | THEN it accepts the binary_data and returns it
104 | """
105 | binary_data = b"test"
106 |
107 | # request with raw data
108 | headers = {"content-type": "image/jpeg"}
109 | resp = requests.post(url, data=binary_data, headers=headers)
110 | assert resp.ok
111 | assert resp.content == b'"test"'
112 |
113 | # request mulitpart/form-data
114 | file = {"audio": ("test", binary_data)}
115 | resp = requests.post(url.format("fileapi"), files=file)
116 | assert resp.ok
117 | assert resp.content == b'"test"'
118 |
119 |
120 | if __name__ == "__main__":
121 |
122 | setup = Setup()
123 | failed = False
124 | try:
125 | url = setup.make_deployment()
126 | except Exception as e:
127 | print("Setup failed")
128 | raise e
129 | else:
130 | # setup successful!
131 | print("Setup successful")
132 |
133 | # list of tests to perform
134 | TESTS = [(test_json, "jsonapi"), (test_df, "dfapi")]
135 |
136 | for test_func, endpoint in TESTS:
137 | try:
138 | print("Testing endpoint /{}...".format(endpoint), end="")
139 | test_func(url.format(endpoint))
140 | print("\033[92m passed! \033[0m")
141 | except Exception as e:
142 | print("\033[91m failed! \033[0m")
143 | print("\nTest at endpoint /{} failded: ".format(endpoint), e)
144 | failed = True
145 | finally:
146 | setup.teardown()
147 |
148 | if failed:
149 | sys.exit(1)
150 | else:
151 | sys.exit(0)
152 |
--------------------------------------------------------------------------------